chore: sync from fn-registry agent

This commit is contained in:
fn-registry agent
2026-05-11 16:28:44 +02:00
commit f1e2c1cd19
25 changed files with 3973 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
add_imgui_app(primitives_gallery
main.cpp
capture.cpp
demo.cpp
demos_core.cpp
demos_viz.cpp
demos_graph.cpp
demos_graph_styles.cpp
demos_gfx.cpp
demos_3d.cpp
demos_text_editor.cpp
demos_gl_texture.cpp
demos_extras.cpp
demos_mesh.cpp
# animation primitives (issue 0031)
demos_animation.cpp
${CMAKE_SOURCE_DIR}/functions/core/tween_curves.cpp
${CMAKE_SOURCE_DIR}/functions/core/bezier_editor.cpp
${CMAKE_SOURCE_DIR}/functions/core/timeline.cpp
demos_sql.cpp
demos_scientific.cpp
# text_editor + file_watcher (issue 0025) + file_poll_diff pure (issue 0045)
${CMAKE_SOURCE_DIR}/functions/core/text_editor.cpp
${CMAKE_SOURCE_DIR}/functions/core/file_watcher.cpp
${CMAKE_SOURCE_DIR}/functions/core/file_poll_diff.cpp
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit/TextEditor.cpp
# sql_workbench (issue 0032) + sql_parse pure (issue 0045)
${CMAKE_SOURCE_DIR}/functions/core/sql_workbench.cpp
${CMAKE_SOURCE_DIR}/functions/core/sql_parse.cpp
# Core primitives demoed (tokens vive en fn_framework)
${CMAKE_SOURCE_DIR}/functions/core/fullscreen_window.cpp
${CMAKE_SOURCE_DIR}/functions/core/page_header.cpp
${CMAKE_SOURCE_DIR}/functions/core/dashboard_panel.cpp
${CMAKE_SOURCE_DIR}/functions/core/badge.cpp
${CMAKE_SOURCE_DIR}/functions/core/empty_state.cpp
${CMAKE_SOURCE_DIR}/functions/core/button.cpp
${CMAKE_SOURCE_DIR}/functions/core/icon_button.cpp
${CMAKE_SOURCE_DIR}/functions/core/toolbar.cpp
${CMAKE_SOURCE_DIR}/functions/core/modal_dialog.cpp
${CMAKE_SOURCE_DIR}/functions/core/text_input.cpp
${CMAKE_SOURCE_DIR}/functions/core/select.cpp
${CMAKE_SOURCE_DIR}/functions/core/toast.cpp
${CMAKE_SOURCE_DIR}/functions/core/tree_view.cpp
${CMAKE_SOURCE_DIR}/functions/core/process_runner.cpp
${CMAKE_SOURCE_DIR}/functions/core/process_state_machine.cpp
# Viz primitives demoed
${CMAKE_SOURCE_DIR}/functions/viz/kpi_card.cpp
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
${CMAKE_SOURCE_DIR}/functions/viz/pie_chart.cpp
${CMAKE_SOURCE_DIR}/functions/viz/line_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/scatter_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/histogram.cpp
${CMAKE_SOURCE_DIR}/functions/viz/sparkline.cpp
${CMAKE_SOURCE_DIR}/functions/viz/candlestick.cpp
${CMAKE_SOURCE_DIR}/functions/viz/gauge.cpp
${CMAKE_SOURCE_DIR}/functions/viz/heatmap.cpp
${CMAKE_SOURCE_DIR}/functions/viz/table_view.cpp
# 3D viz primitives (issue 0028, ImPlot3D)
${CMAKE_SOURCE_DIR}/functions/viz/surface_plot_3d.cpp
${CMAKE_SOURCE_DIR}/functions/viz/scatter_3d.cpp
# Scientific viz (issue 0034)
${CMAKE_SOURCE_DIR}/functions/viz/treemap.cpp
${CMAKE_SOURCE_DIR}/functions/viz/sankey.cpp
${CMAKE_SOURCE_DIR}/functions/viz/chord.cpp
${CMAKE_SOURCE_DIR}/functions/viz/contour.cpp
${CMAKE_SOURCE_DIR}/functions/viz/voronoi.cpp
# Graph stack (instanced GPU + Barnes-Hut + spatial hash)
${CMAKE_SOURCE_DIR}/functions/viz/graph_types.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_renderer.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_icons.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_force_layout.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_force_layout_gpu.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_layouts.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_viewport.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_viewport_selection.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_labels.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_labels_select.cpp
${CMAKE_SOURCE_DIR}/functions/core/graph_spatial_hash.cpp
# GL loader (Linux no-op, Windows wglGetProcAddress)
${CMAKE_SOURCE_DIR}/functions/gfx/gl_loader.cpp
# Shader stack (shader_canvas demo)
${CMAKE_SOURCE_DIR}/functions/gfx/gl_shader.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/gl_framebuffer.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/fullscreen_quad.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/shader_canvas.cpp
# gl_texture_load (issue 0026) + stb_image
${CMAKE_SOURCE_DIR}/functions/gfx/gl_texture_load.cpp
${CMAKE_SOURCE_DIR}/vendor/stb/stb_image_impl.cpp
# mesh_viewer stack (issue 0029)
${CMAKE_SOURCE_DIR}/functions/gfx/mesh_obj_load.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/mesh_gpu.cpp
${CMAKE_SOURCE_DIR}/functions/core/orbit_camera.cpp
${CMAKE_SOURCE_DIR}/functions/viz/mesh_viewer.cpp
)
target_include_directories(primitives_gallery PRIVATE
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit
${CMAKE_SOURCE_DIR}/vendor/stb
)
# SQLite (sql_workbench) — alias provisto por cpp/CMakeLists.txt:
# system on Linux, vendored amalgamation on Windows cross-compile.
target_link_libraries(primitives_gallery PRIVATE SQLite::SQLite3)
if(WIN32)
target_link_libraries(primitives_gallery PRIVATE opengl32)
endif()
if(WIN32)
set_target_properties(primitives_gallery PROPERTIES WIN32_EXECUTABLE TRUE)
endif()
+159
View File
@@ -0,0 +1,159 @@
# primitives_gallery
Catalogo visual interactivo de los primitivos UI del registry (`cpp/functions/core` y `cpp/functions/viz`). Un solo ejecutable con sidebar izquierdo + panel derecho que renderiza la demo del primitivo seleccionado con todas sus variantes y un snippet de codigo.
## Rol
| Funcion | Como lo cumple |
|---|---|
| Smoke test visual | Abrir la gallery tras un cambio en tokens / componentes; si algo se ve raro, lo cazas en segundos. |
| Documentacion viva | Cada demo muestra el componente trabajando + el snippet exacto. Mas rapido que leer los `.md`. |
| Build gate | Esta en el CMake principal (`cpp/CMakeLists.txt`). Si un primitivo rompe API, la gallery no compila => CI rojo. |
| Sandbox de prototipos | Datos sinteticos, sin backend; ideal para iterar un primitivo nuevo sin tocar el dashboard. |
## Build & run
```bash
# Linux
cmake --build cpp/build/linux --target primitives_gallery -j$(nproc)
./cpp/build/linux/apps/primitives_gallery/primitives_gallery
# Windows (cross-compile)
cmake --build cpp/build/windows --target primitives_gallery -j$(nproc)
# binario: cpp/build/windows/apps/primitives_gallery/primitives_gallery.exe
```
No se conecta a `sqlite_api` ni a ningun backend. Datos sinteticos generados in-memory.
## Demos disponibles
### Core
| Demo | Primitivo | Que muestra |
|---|---|---|
| button | `button_cpp_core` | 4 variantes x 3 sizes |
| icon_button | `icon_button_cpp_core` | Glyphs comunes con tooltip |
| toolbar | `toolbar_cpp_core` | Dos grupos con separador vertical |
| modal_dialog | `modal_dialog_cpp_core` | Boton que abre modal con form |
| text_input | `text_input_cpp_core` | 3 inputs con placeholder |
| select | `select_cpp_core` | Dropdown con y sin `(none)` |
| toast + inbox | `toast_cpp_core` (v1.1) | 4 botones que disparan toasts + campana con badge |
| tree_view | `tree_view_cpp_core` | Arbol fake de proyectos -> apps |
| badge | `badge_cpp_core` | 6 variantes semanticas |
| empty_state | `empty_state_cpp_core` | Lista vacia con icono + cta |
| page_header | `page_header_cpp_core` | Header con toolbar a la derecha |
| dashboard_panel | `dashboard_panel_cpp_core` | Panel con titulo y borde |
| kpi_card | `kpi_card_cpp_viz` (v1.2) | Grid 1x4 con sparklines y delta |
### Viz
| Demo | Primitivo | Que muestra |
|---|---|---|
| bar_chart | `bar_chart_cpp_viz` (v1.2) | Labels que caben + labels rotados 45 |
| pie_chart | `pie_chart_cpp_viz` (v1.1) | Pie + donut con tooltip por slice |
| line_plot | `line_plot_cpp_viz` (v1.1) | Serie sintetica `sin(t) + ruido` |
| scatter_plot | `scatter_plot_cpp_viz` (v1.1) | 120 puntos con correlacion |
| histogram | `histogram_cpp_viz` (v1.1) | 300 muestras gaussianas |
| sparkline | `sparkline_cpp_viz` | Trending up / down / flat |
| graph_viewport | `graph_viewport_cpp_viz` | **Ver seccion abajo** |
## Demo `graph_viewport` (en detalle)
Pipeline completo de visualizacion de grafos con instanced GPU rendering:
- `graph_renderer_cpp_viz` (1 draw call para todos los nodos via `glDrawArraysInstanced`)
- `graph_force_layout_cpp_viz` (Barnes-Hut, paso de simulacion por frame)
- `graph_spatial_hash_cpp_core` (hit-testing O(1) bajo el cursor)
- `graph_viewport_cpp_viz` (widget que orquesta los anteriores con pan/zoom/select)
### Controles
| Control | Rango | Efecto |
|---|---|---|
| `Nodes` | 100 20 000 | Numero de nodos a generar |
| `Clusters` | 2 16 | Numero de comunidades (cada una con su color) |
| `Repulsion` | 100 20 000 | Fuerza repulsiva entre todos los nodos. Mas alto => grafo mas extendido y energia mayor. |
| `Attraction` | 0.001 0.5 | Constante del muelle de las aristas. Mas alto => clusters mas compactos. |
| `Gravity` | 0.0 0.05 | Tiron hacia (0,0). Util para evitar drift cuando subes mucho la repulsion. |
| `Regenerate` | boton | Regenera el grafo con los valores actuales de Nodes/Clusters. |
| `Pause / Resume layout` | boton | Para o reanuda la simulacion force-directed. |
| `Fit view` | boton | Encuadra la camara al bounding box del grafo con 10% de padding. |
Los tres sliders de fuerzas se leen cada frame y se inyectan en `ForceLayoutConfig`, asi que cambiar un valor durante el layout en marcha re-calibra el sistema al instante.
### Stats line (sin vibracion)
Una sola linea fija — sin secciones condicionales que cambien la altura del panel:
```
nodes=N edges=E energy=X fps=F | hover=#id cN sel=#id
```
`hover` y `sel` muestran `-` cuando no hay nada seleccionado para mantener el ancho/alto estable; antes una fila condicional desplazaba el viewport en cada hover.
### Interaccion con el viewport
| Gesto | Accion |
|---|---|
| Drag con boton izquierdo en zona vacia | Pan de camara |
| Wheel | Zoom (limites 0.01x 50x) |
| Drag sobre nodo | Mueve el nodo (lo `pin`ea durante el drag) |
| Click sobre nodo | Selecciona (`s_state.selected_node`) |
| Hover sobre nodo | Resaltado + `s_state.hovered_node` poblado |
### Datos sinteticos
`generate_synthetic_graph(N, K)` reparte N nodos en K clusters dispuestos en circulo, con ~3 aristas intra-cluster por nodo y un 5% adicional de aristas inter-cluster. Paleta de 8 colores ABGR. Posiciones iniciales con dispersion gaussiana de 80 px alrededor del centroide del cluster — el force layout las reordena en pocos frames.
### Performance esperada
| Nodes | FPS objetivo (RTX 30xx, viewport 800x460) | Notas |
|---|---|---|
| 1 000 | 60 (vsync) | Caso comun; layout converge < 1 s |
| 5 000 | 60 | Pipeline al limite del CPU para Barnes-Hut |
| 20 000 | 30 50 | El cuello pasa a ser el layout (CPU); GPU render sigue holgado |
Si necesitas mas, fija los nodos (`pinned = true` o `Pause layout`) y veras 60 fps estables — el bottleneck es la simulacion, no el render.
## Anadir un demo nuevo
1. Anadir el prototipo en `demos.h` dentro de `namespace gallery`:
```cpp
void demo_my_thing();
```
2. Implementar el cuerpo en `demos_core.cpp` o `demos_viz.cpp` (o un fichero nuevo si la demo es grande, p.ej. `demos_graph.cpp`).
3. Registrar la entrada en el array `k_demos[]` de `main.cpp`:
```cpp
{"my_thing", "my_thing", "Core" /* o "Viz" */, &gallery::demo_my_thing},
```
4. Si la demo necesita `.cpp` adicionales del registry, anadirlos a `CMakeLists.txt` de la gallery.
5. Recompilar.
## Estructura
```
cpp/apps/primitives_gallery/
CMakeLists.txt # target primitives_gallery
README.md # este fichero
main.cpp # sidebar + router
demo.{h,cpp} # helpers (demo_header, section, code_block, ...)
demos.h # prototipos void demo_xxx()
demos_core.cpp # demos del dominio core
demos_viz.cpp # demos del dominio viz (charts simples)
demos_graph.cpp # demo de graph_viewport (mas pesada, fichero aparte)
```
## Convenciones para los demos
- **Sin estado real**: usar arrays sinteticos (`float fake[] = {...}`) o generadores deterministas con seed fijo. Datos reproducibles.
- **Sin red**: nunca llamar a `sqlite_api`, HTTP, filesystem. La gallery debe arrancar offline en cualquier maquina.
- **Snippets honestos**: el `code_block(...)` debe mostrar el codigo que produce esa demo, no pseudocodigo.
- **Variantes en grids**: si un primitivo tiene N variantes x M tamanos, mostrarlos todos en un `BeginTable` para comparacion lado-a-lado.
- **Estado static**: si la demo es interactiva (sliders, modal, etc.), guardar el estado en `static` locales — la gallery no destruye demos al cambiar de seccion, asi que el estado persiste hasta cerrar la app.
## Iconos en los demos
A partir de la sesion 2026-04-25 los demos usan los macros `TI_*` de `cpp/functions/core/icons_tabler.h` (Tabler v3.41.1, 5093 glyphs). La fuente la carga automaticamente `fn::run_app` via `icon_font_cpp_core`, y `add_imgui_app` copia `tabler-icons.ttf` junto al ejecutable post-build (no hay paso manual).
`demo_icon_button` y `demo_toolbar` (en `demos_core.cpp`) son la referencia visual: muestran el patron `button(TI_PLUS " New", V::Primary)` y la fila de iconos sueltos. Ver `cpp/DESIGN_SYSTEM.md` seccion 11 para la regla.
Si añades un demo nuevo y necesitas glyphs, **no metas `\x..` UTF-8 inline** — busca el icono en `icons_tabler.h` (o en https://tabler.io/icons) y usa el `TI_*` correspondiente.
+37
View File
@@ -0,0 +1,37 @@
---
name: primitives_gallery
lang: cpp
domain: gfx
description: "Visual catalog de primitivas C++ UI del fn_registry. Demos por categoria (charts, controls, layout, gl_info). Soporta modo --capture para regresion visual."
tags: [imgui, gallery, gfx, demo, capture]
uses_functions: []
uses_types: []
framework: "imgui"
entry_point: "main.cpp"
dir_path: "cpp/apps/primitives_gallery"
repo_url: ""
---
# primitives_gallery
Catalogo visual de las primitivas y componentes ImGui del registry. Cada demo se carga al hacer click en su entrada del sidebar.
## Build & run
```bash
cd cpp && cmake --build build --target primitives_gallery -j
./build/primitives_gallery
```
## Modo capture (regresion visual)
```bash
./build/primitives_gallery --capture <out_dir>
```
Renderiza cada demo offscreen y guarda PNGs en `<out_dir>/`. Permite gate visual via golden images.
## Notas
- `auto_dockspace = false` — usa `fullscreen_window` que ocupa todo el viewport.
- `init_gl_loader = true` — necesario para demos de OpenGL 4.3 core (compute, SSBOs).
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 966 B

+173
View File
@@ -0,0 +1,173 @@
// Implementacion de gallery::run_capture — render offscreen + glReadPixels +
// PNG via stb_image_write. Ver capture.h.
#include "capture.h"
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include "implot.h"
#include "implot3d.h"
#include "core/tokens.h"
#include "core/icon_font.h"
#include "core/app_settings.h"
#include "gfx/gl_loader.h"
#include <GLFW/glfw3.h>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#include <cstdio>
#include <vector>
namespace gallery {
static void glfw_capture_error(int error, const char* description) {
std::fprintf(stderr, "GLFW Error %d: %s\n", error, description);
}
// Flip vertical in-place: OpenGL origin = bottom-left, PNG = top-left.
static void flip_vertical_rgba(unsigned char* px, int w, int h) {
const int stride = w * 4;
std::vector<unsigned char> row(stride);
for (int y = 0; y < h / 2; ++y) {
unsigned char* a = px + y * stride;
unsigned char* b = px + (h - 1 - y) * stride;
std::copy(a, a + stride, row.begin());
std::copy(b, b + stride, a);
std::copy(row.begin(), row.end(), b);
}
}
bool run_capture(const CaptureConfig& cfg, const std::vector<CaptureItem>& items) {
glfwSetErrorCallback(&glfw_capture_error);
if (!glfwInit()) {
std::fprintf(stderr, "capture: glfwInit failed\n");
return false;
}
// Capture mode usa GL 3.3 deliberadamente: WSL Mesa no entrega contexto
// 4.3 offscreen (GLXBadFBConfig). Las pruebas visuales no necesitan
// compute/SSBO — ImGui+ImPlot funciona en 3.3 core. La build interactiva
// (app_base.cpp) si pide 4.3.
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
GLFWwindow* window = glfwCreateWindow(
cfg.capture_w, cfg.capture_h, "capture", nullptr, nullptr);
if (!window) {
std::fprintf(stderr, "capture: glfwCreateWindow failed (no GL?)\n");
glfwTerminate();
return false;
}
glfwMakeContextCurrent(window);
glfwSwapInterval(0);
if (!fn::gfx::gl_loader_init()) {
std::fprintf(stderr, "capture: gl_loader_init failed\n");
glfwDestroyWindow(window);
glfwTerminate();
return false;
}
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImPlot::CreateContext();
ImPlot3D::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.IniFilename = nullptr; // no .ini side effects in capture mode.
io.DisplaySize = ImVec2((float)cfg.capture_w, (float)cfg.capture_h);
fn_ui::settings_load();
fn_ui::load_fonts_from_settings();
{
ImGuiStyle& style = ImGui::GetStyle();
style.FontSizeBase = fn_ui::settings().font_size_px;
style._NextFrameFontSizeBase = style.FontSizeBase;
}
fn_tokens::apply_dark_theme();
ImGui_ImplGlfw_InitForOpenGL(window, false);
ImGui_ImplOpenGL3_Init("#version 330");
bool ok_all = true;
std::vector<unsigned char> pixels((size_t)cfg.capture_w * cfg.capture_h * 4u);
for (const auto& item : items) {
// Warmup: rinde varios frames para que ImGui/ImPlot estabilicen layout
// (el primer frame frecuentemente carece de mediciones de tamaño).
for (int frame = 0; frame < cfg.warmup_frames + 1; ++frame) {
glfwPollEvents();
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
// Ventana fullscreen sobre el viewport con la demo activa,
// sin sidebar (queremos el render del primitivo lo mas limpio
// posible para el diff visual).
const ImGuiViewport* vp = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(vp->WorkPos);
ImGui::SetNextWindowSize(vp->WorkSize);
ImGui::Begin("##capture_root",
nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoSavedSettings);
if (item.fn) item.fn();
ImGui::End();
ImGui::Render();
int dw, dh;
glfwGetFramebufferSize(window, &dw, &dh);
glViewport(0, 0, dw, dh);
glClearColor(fn_tokens::colors::bg.x,
fn_tokens::colors::bg.y,
fn_tokens::colors::bg.z, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(window);
}
// Read framebuffer (GL_RGBA / GL_UNSIGNED_BYTE).
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, cfg.capture_w, cfg.capture_h,
GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
flip_vertical_rgba(pixels.data(), cfg.capture_w, cfg.capture_h);
char path[1024];
std::snprintf(path, sizeof(path), "%s/%s.png",
cfg.output_dir.c_str(), item.id.c_str());
const int rc = stbi_write_png(
path, cfg.capture_w, cfg.capture_h, 4,
pixels.data(), cfg.capture_w * 4);
if (rc == 0) {
std::fprintf(stderr, "capture: stbi_write_png failed for %s\n", path);
ok_all = false;
} else {
std::fprintf(stdout, "captured: %s\n", path);
}
}
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImPlot3D::DestroyContext();
ImPlot::DestroyContext();
ImGui::DestroyContext();
glfwDestroyWindow(window);
glfwTerminate();
return ok_all;
}
} // namespace gallery
+34
View File
@@ -0,0 +1,34 @@
#pragma once
// Capture mode: renderiza cada demo de la gallery en una ventana GLFW
// invisible y guarda un PNG en `output_dir/<demo_id>.png` via stb_image_write.
//
// Diseñado para CI / golden-image diffing: ver `cpp/scripts/update_goldens.sh`
// y `cpp/tests/test_visual.cpp`.
//
// Importante:
// - Requiere un contexto OpenGL real. En entornos sin GPU (containers minimos)
// funciona con `LIBGL_ALWAYS_SOFTWARE=1` (Mesa/llvmpipe) o swiftshader.
// - Si el entorno (WSL sin GL) no puede crear un contexto GL valido, el
// binario sale con codigo != 0 sin generar PNGs.
#include <string>
#include <vector>
namespace gallery {
struct CaptureItem {
std::string id;
void (*fn)();
};
struct CaptureConfig {
std::string output_dir;
int warmup_frames = 3;
int capture_w = 800;
int capture_h = 600;
};
// Devuelve true si todo el set se capturo OK.
bool run_capture(const CaptureConfig& cfg, const std::vector<CaptureItem>& items);
} // namespace gallery
+76
View File
@@ -0,0 +1,76 @@
#include "demo.h"
#include "core/tokens.h"
#include <cstdio>
namespace gallery {
void demo_header(const char* name, const char* version, const char* description) {
using namespace fn_tokens;
ImGui::SetWindowFontScale(1.4f);
ImGui::TextUnformatted(name);
ImGui::SetWindowFontScale(1.0f);
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::Text(" %s", version);
ImGui::PopStyleColor();
if (description && *description) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::TextWrapped("%s", description);
ImGui::PopStyleColor();
}
ImGui::Separator();
ImGui::Dummy(ImVec2(0, spacing::sm));
}
void section(const char* title) {
using namespace fn_tokens;
ImGui::Dummy(ImVec2(0, spacing::sm));
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::TextUnformatted(title);
ImGui::PopStyleColor();
ImGui::Separator();
ImGui::Dummy(ImVec2(0, spacing::xs));
}
void variant_label(const char* text) {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim);
ImGui::TextUnformatted(text);
ImGui::PopStyleColor();
}
void code_block(const char* code) {
using namespace fn_tokens;
ImGui::Dummy(ImVec2(0, spacing::sm));
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::TextUnformatted("// example");
ImGui::PopStyleColor();
ImGui::PushStyleColor(ImGuiCol_ChildBg, colors::bg);
ImGui::PushStyleColor(ImGuiCol_Border, colors::border);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, radius::sm);
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::md, spacing::sm));
// Altura: aprox lineas * line-height
int lines = 1;
for (const char* p = code; *p; ++p) if (*p == '\n') ++lines;
float h = lines * ImGui::GetTextLineHeightWithSpacing() + spacing::md;
char id[32];
std::snprintf(id, sizeof(id), "##code_%p", (const void*)code);
ImGui::BeginChild(id, ImVec2(0, h),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PushStyleColor(ImGuiCol_Text, colors::text);
ImGui::TextUnformatted(code);
ImGui::PopStyleColor();
ImGui::EndChild();
ImGui::PopStyleVar(3);
ImGui::PopStyleColor(2);
}
} // namespace gallery
+22
View File
@@ -0,0 +1,22 @@
#pragma once
// Helpers compartidos por todas las demos de la gallery.
// No son primitivos del registry — son utilidades locales de este app.
#include "imgui.h"
#include <string>
namespace gallery {
// Titulo + version + descripcion en la parte superior del panel derecho.
void demo_header(const char* name, const char* version, const char* description);
// Seccion secundaria dentro de una demo (agrupar variantes).
void section(const char* title);
// Bloque de codigo monoespaciado con bg surface y label "// example".
void code_block(const char* code);
// Etiqueta sutil encima de un grupo de widgets.
void variant_label(const char* text);
} // namespace gallery
+56
View File
@@ -0,0 +1,56 @@
#pragma once
// Cada demo_xxx() renderiza una seccion completa para un primitivo.
// Se llaman desde main.cpp en funcion del item seleccionado en el sidebar.
namespace gallery {
// --- Core ---
void demo_button();
void demo_icon_button();
void demo_toolbar();
void demo_modal();
void demo_text_input();
void demo_select();
void demo_toast();
void demo_tree_view();
void demo_kpi_card();
void demo_badge();
void demo_empty_state();
void demo_page_header();
void demo_dashboard_panel();
void demo_text_editor(); // wave 1, issue 0025
void demo_file_watcher(); // wave 1, issue 0025
void demo_process_runner();
void demo_tween(); // issue 0031
void demo_bezier_editor(); // issue 0031
void demo_timeline(); // issue 0031
void demo_sql_workbench(); // issue 0032
// --- Viz ---
void demo_bar_chart();
void demo_pie_chart();
void demo_line_plot();
void demo_scatter_plot();
void demo_histogram();
void demo_sparkline();
void demo_graph();
void demo_graph_styles(); // issue 0049f
void demo_candlestick();
void demo_gauge();
void demo_heatmap();
void demo_table_view();
void demo_surface_plot_3d(); // issue 0028, ImPlot3D
void demo_scatter_3d(); // issue 0028, ImPlot3D
void demo_mesh_viewer(); // issue 0029
void demo_treemap(); // issue 0034
void demo_sankey(); // issue 0034
void demo_chord(); // issue 0034
void demo_contour(); // issue 0034
void demo_voronoi(); // issue 0034
// --- Gfx ---
void demo_shader_canvas();
void demo_gl_texture(); // wave 1, issue 0026
void demo_gl_info(); // issue 0049b — runtime GL version + 4.3 caps
} // namespace gallery
+100
View File
@@ -0,0 +1,100 @@
// demos_3d — demos para los primitivos viz/* basados en ImPlot3D.
// Issue 0028: surface_plot_3d real + scatter_3d.
#include "demos.h"
#include "demo.h"
#include "viz/surface_plot_3d.h"
#include "viz/scatter_3d.h"
#include <imgui.h>
#include <cmath>
#include <random>
#include <vector>
namespace gallery {
// ---------------------------------------------------------------------------
// surface_plot_3d
// ---------------------------------------------------------------------------
void demo_surface_plot_3d() {
demo_header("surface_plot_3d", "v2.0.0",
"Superficie 3D ImPlot3D (z = A * sin(fx*x) * cos(fy*y)) con sliders para "
"ajustar las frecuencias en tiempo real. Drag para orbitar, wheel para zoom.");
section("Malla 64x64 — sin(fx*x) * cos(fy*y)");
static float fx = 0.20f;
static float fy = 0.20f;
static float amp = 1.0f;
ImGui::SliderFloat("fx", &fx, 0.05f, 1.0f, "%.2f");
ImGui::SliderFloat("fy", &fy, 0.05f, 1.0f, "%.2f");
ImGui::SliderFloat("amplitud", &amp, 0.1f, 3.0f, "%.2f");
constexpr int N = 64;
static std::vector<float> z(N * N);
for (int j = 0; j < N; ++j) {
for (int i = 0; i < N; ++i) {
z[j * N + i] = amp * std::sin(fx * float(i)) * std::cos(fy * float(j));
}
}
fn::SurfacePlot3DConfig cfg{};
cfg.z = z.data();
cfg.nx = N; cfg.ny = N;
cfg.x_min = 0.f; cfg.x_max = float(N);
cfg.y_min = 0.f; cfg.y_max = float(N);
cfg.size = ImVec2(-1.f, 420.f);
fn::surface_plot_3d("##gallery_surface", cfg);
}
// ---------------------------------------------------------------------------
// scatter_3d
// ---------------------------------------------------------------------------
void demo_scatter_3d() {
demo_header("scatter_3d", "v1.0.0",
"Scatter 3D ImPlot3D con color por punto. 3 clusters gaussianos sinteticos "
"(N=500) para simular una visualizacion tipica de PCA / clustering.");
section("3 clusters gaussianos (500 puntos)");
constexpr int N = 500;
static std::vector<float> xs(N), ys(N), zs(N);
static std::vector<ImU32> colors(N);
static bool initialized = false;
if (!initialized) {
std::mt19937 rng(42);
std::normal_distribution<float> g(0.f, 0.4f);
const ImU32 palette[3] = {
IM_COL32(255, 99, 71, 255), // tomate
IM_COL32( 65, 170, 255, 255), // azul
IM_COL32(120, 220, 120, 255), // verde
};
const float cx[3] = {-1.5f, 1.5f, 0.f};
const float cy[3] = { 0.f, 0.f, 2.0f};
const float cz[3] = { 0.f, 1.0f,-1.0f};
for (int i = 0; i < N; ++i) {
int c = i % 3;
xs[i] = cx[c] + g(rng);
ys[i] = cy[c] + g(rng);
zs[i] = cz[c] + g(rng);
colors[i] = palette[c];
}
initialized = true;
}
fn::Scatter3DConfig cfg{};
cfg.xs = xs.data();
cfg.ys = ys.data();
cfg.zs = zs.data();
cfg.colors = colors.data();
cfg.n = N;
cfg.size = ImVec2(-1.f, 420.f);
fn::scatter_3d("##gallery_clusters", cfg);
}
} // namespace gallery
+249
View File
@@ -0,0 +1,249 @@
// Demos para los primitivos de animacion (issue 0031):
// - tween_curves
// - bezier_editor
// - timeline
#include "demos.h"
#include "demo.h"
#include "core/tween_curves.h"
#include "core/bezier_editor.h"
#include "core/timeline.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cstdio>
#include <cmath>
namespace gallery {
// ---------------------------------------------------------------------------
// demo_tween — dropdown + plot animado
// ---------------------------------------------------------------------------
void demo_tween() {
using namespace fn_tokens;
using fn::tween::Ease;
demo_header("tween_curves", "v1.0.0",
"Funciones de easing (Penner): linear, quad, cubic, expo, elastic, "
"bounce con variantes in/out/inOut. Header-mostly: el compilador "
"inlinea cada curva en el sitio de llamada.");
section("Selector + plot");
static int ease_idx = (int)Ease::OutCubic;
static float anim_t = 0.0f;
anim_t += ImGui::GetIO().DeltaTime * 0.5f;
if (anim_t > 1.5f) anim_t = -0.25f; // hold un poco antes de reiniciar
// Build labels
const char* labels[fn::tween::ease_count];
for (int i = 0; i < fn::tween::ease_count; i++) {
labels[i] = fn::tween::name((Ease)i);
}
ImGui::SetNextItemWidth(220.0f);
ImGui::Combo("##tween_ease", &ease_idx, labels, fn::tween::ease_count);
Ease ease = (Ease)ease_idx;
float t_clamped = anim_t;
if (t_clamped < 0.0f) t_clamped = 0.0f;
if (t_clamped > 1.0f) t_clamped = 1.0f;
float v = fn::tween::apply(ease, t_clamped);
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::Text(" t=%.2f f(t)=%.3f", t_clamped, v);
ImGui::PopStyleColor();
// Canvas plot
ImVec2 canvas_min = ImGui::GetCursorScreenPos();
ImVec2 canvas_size(360.0f, 220.0f);
ImVec2 canvas_max = ImVec2(canvas_min.x + canvas_size.x, canvas_min.y + canvas_size.y);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(canvas_min, canvas_max, ImGui::GetColorU32(colors::bg), radius::sm);
dl->AddRect(canvas_min, canvas_max, ImGui::GetColorU32(colors::border), radius::sm);
auto to_px = [&](float tx, float ty) {
// ty puede salir de [0,1] (elastic/bounce); damos algo de margen vertical.
return ImVec2(canvas_min.x + tx * canvas_size.x,
canvas_min.y + (1.0f - ty) * canvas_size.y);
};
// Grid 4x4
ImU32 grid = ImGui::GetColorU32(colors::border);
for (int i = 1; i < 4; i++) {
float fx = canvas_min.x + canvas_size.x * (float)i / 4.0f;
float fy = canvas_min.y + canvas_size.y * (float)i / 4.0f;
dl->AddLine(ImVec2(fx, canvas_min.y), ImVec2(fx, canvas_max.y), grid);
dl->AddLine(ImVec2(canvas_min.x, fy), ImVec2(canvas_max.x, fy), grid);
}
// Diagonal linear
dl->AddLine(to_px(0.0f, 0.0f), to_px(1.0f, 1.0f),
ImGui::GetColorU32(colors::text_dim), 1.0f);
// Curva
constexpr int N = 96;
ImVec2 prev = to_px(0.0f, fn::tween::apply(ease, 0.0f));
ImU32 col = ImGui::GetColorU32(colors::primary);
for (int i = 1; i <= N; i++) {
float x = (float)i / (float)N;
float y = fn::tween::apply(ease, x);
ImVec2 cur = to_px(x, y);
dl->AddLine(prev, cur, col, 2.0f);
prev = cur;
}
// Marker animado
ImVec2 m = to_px(t_clamped, v);
dl->AddCircleFilled(m, 5.0f, ImGui::GetColorU32(colors::primary_light));
dl->AddCircle(m, 6.0f, ImGui::GetColorU32(colors::text), 0, 1.5f);
// Avanzar cursor
ImGui::Dummy(canvas_size);
code_block(
"#include \"core/tween_curves.h\"\n\n"
"float k = fn::tween::apply(fn::tween::Ease::OutCubic, t);\n"
"// o named:\n"
"float k2 = fn::tween::out_cubic(t);"
);
}
// ---------------------------------------------------------------------------
// demo_bezier_editor — editor + plot evaluado
// ---------------------------------------------------------------------------
void demo_bezier_editor() {
using namespace fn_tokens;
demo_header("bezier_editor", "v1.0.0",
"Editor visual de curva Bezier cubica (4 puntos). Para diseñar "
"easing curves custom. p1/p2 son draggable; p0/p3 fijos en (0,0)/(1,1).");
section("Editor");
static fn::BezierCurve curve; // identidad por defecto: ease lineal con handles desplazados
if (ImGui::Button("Reset##bz_reset")) {
curve = fn::BezierCurve{};
}
ImGui::SameLine();
if (ImGui::Button("Ease-out preset##bz_eo")) {
curve = {{0,0}, {0.0f, 0.0f}, {0.58f, 1.0f}, {1,1}};
}
ImGui::SameLine();
if (ImGui::Button("Ease-in-out preset##bz_eio")) {
curve = {{0,0}, {0.42f, 0.0f}, {0.58f, 1.0f}, {1,1}};
}
fn::bezier_editor("##bz_editor", curve, ImVec2(220, 220));
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::Text("p0=(%.2f,%.2f) p1=(%.2f,%.2f) p2=(%.2f,%.2f) p3=(%.2f,%.2f)",
curve.p0.x, curve.p0.y, curve.p1.x, curve.p1.y,
curve.p2.x, curve.p2.y, curve.p3.x, curve.p3.y);
ImGui::PopStyleColor();
// Plot evaluation
section("bezier_eval(curve, t)");
static float t = 0.0f;
ImGui::SetNextItemWidth(360.0f);
ImGui::SliderFloat("t##bz_t", &t, 0.0f, 1.0f, "%.3f");
float y = fn::bezier_eval(curve, t);
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::Text("y(t=%.3f) = %.3f", t, y);
ImGui::PopStyleColor();
code_block(
"#include \"core/bezier_editor.h\"\n\n"
"static fn::BezierCurve curve;\n"
"if (fn::bezier_editor(\"##my\", curve, ImVec2(220, 220))) {\n"
" // user dragged a control point\n"
"}\n"
"float k = fn::bezier_eval(curve, t);"
);
}
// ---------------------------------------------------------------------------
// demo_timeline — 2 tracks + display
// ---------------------------------------------------------------------------
void demo_timeline() {
using namespace fn_tokens;
using fn::tween::Ease;
demo_header("timeline", "v1.0.0",
"Timeline tipo DAW: tracks horizontales con keyframes draggable, "
"scrub con el ruler, play/pause/loop. track_value_at(t) interpola "
"aplicando la Ease de cada keyframe destino.");
static fn::TimelineState tl;
static bool inited = false;
if (!inited) {
tl.duration = 4.0f;
tl.playing = true;
tl.tracks.push_back({"hue", {
{0.0f, 0.0f, Ease::Linear},
{2.0f, 1.0f, Ease::OutCubic},
{4.0f, 0.0f, Ease::InOutCubic},
}});
tl.tracks.push_back({"amp", {
{0.0f, 0.2f, Ease::Linear},
{3.0f, 1.0f, Ease::OutElastic},
}});
inited = true;
}
// Update
fn::timeline_update(tl, ImGui::GetIO().DeltaTime);
// Display values
section("Live values");
float hue = fn::track_value_at(tl.tracks[0], tl.current_time);
float amp = fn::track_value_at(tl.tracks[1], tl.current_time);
ImGui::PushStyleColor(ImGuiCol_Text, colors::text);
ImGui::Text("t = %.3fs", tl.current_time);
ImGui::PopStyleColor();
auto draw_bar = [&](const char* name, float value, float vmin, float vmax) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::Text("%-4s", name);
ImGui::PopStyleColor();
ImGui::SameLine();
ImVec2 cmin = ImGui::GetCursorScreenPos();
ImVec2 csize = ImVec2(280.0f, 14.0f);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(cmin, ImVec2(cmin.x + csize.x, cmin.y + csize.y),
ImGui::GetColorU32(colors::surface_active), radius::sm);
float k = (value - vmin) / (vmax - vmin);
if (k < 0.0f) k = 0.0f;
if (k > 1.0f) k = 1.0f;
dl->AddRectFilled(cmin, ImVec2(cmin.x + csize.x * k, cmin.y + csize.y),
ImGui::GetColorU32(colors::primary), radius::sm);
ImGui::Dummy(csize);
ImGui::SameLine();
ImGui::Text("%.3f", value);
};
draw_bar("hue", hue, 0.0f, 1.0f);
draw_bar("amp", amp, 0.0f, 1.0f);
section("Widget");
fn::timeline_widget("##gallery_tl", tl, ImVec2(-1, 220));
code_block(
"#include \"core/timeline.h\"\n\n"
"static fn::TimelineState tl;\n"
"tl.tracks.push_back({\"hue\", {{0,0}, {2,1, fn::tween::Ease::OutCubic}, {4,0}}});\n"
"tl.duration = 4.0f; tl.playing = true;\n\n"
"fn::timeline_update(tl, ImGui::GetIO().DeltaTime);\n"
"float h = fn::track_value_at(tl.tracks[0], tl.current_time);\n"
"fn::timeline_widget(\"##tl\", tl);"
);
}
} // namespace gallery
+447
View File
@@ -0,0 +1,447 @@
#include "demos.h"
#include "demo.h"
#include "core/button.h"
#include "core/icon_button.h"
#include "core/toolbar.h"
#include "core/modal_dialog.h"
#include "core/text_input.h"
#include "core/select.h"
#include "core/toast.h"
#include "core/tree_view.h"
#include "core/badge.h"
#include "core/empty_state.h"
#include "core/page_header.h"
#include "core/dashboard_panel.h"
#include "core/tokens.h"
#include "core/icons_tabler.h"
#include "viz/kpi_card.h"
#include <imgui.h>
#include <cstdio>
using namespace fn_ui;
using V = ButtonVariant;
using S = ButtonSize;
namespace gallery {
// ---------------------------------------------------------------------------
// button
// ---------------------------------------------------------------------------
void demo_button() {
demo_header("button", "v1.0.0",
"Boton con 4 variantes semanticas y 3 tamanos. Usa tokens para colores, "
"radius y padding — estilo consistente en toda la app.");
section("Variants x Sizes");
const V variants[] = {V::Primary, V::Secondary, V::Subtle, V::Danger};
const char* variant_names[] = {"Primary", "Secondary", "Subtle", "Danger"};
const S sizes[] = {S::Sm, S::Md, S::Lg};
const char* size_names[] = {"sm", "md", "lg"};
if (ImGui::BeginTable("##btn_grid", 5, ImGuiTableFlags_SizingFixedFit)) {
ImGui::TableSetupColumn("size");
for (int c = 0; c < 4; c++) ImGui::TableSetupColumn(variant_names[c]);
ImGui::TableHeadersRow();
for (int s = 0; s < 3; s++) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
variant_label(size_names[s]);
for (int v = 0; v < 4; v++) {
ImGui::TableSetColumnIndex(v + 1);
char id[32];
std::snprintf(id, sizeof(id), "%s##%d%d", variant_names[v], s, v);
button(id, variants[v], sizes[s]);
}
}
ImGui::EndTable();
}
code_block(
"#include \"core/button.h\"\n"
"using fn_ui::button;\n"
"using V = fn_ui::ButtonVariant;\n\n"
"if (button(\"Save\", V::Primary)) save();\n"
"if (button(\"Cancel\", V::Subtle)) close();\n"
"if (button(\"Delete\", V::Danger)) confirm();"
);
}
// ---------------------------------------------------------------------------
// icon_button
// ---------------------------------------------------------------------------
void demo_icon_button() {
demo_header("icon_button", "v1.0.0",
"Boton cuadrado 28x28 con un glyph centrado y tooltip opcional. "
"Usa los TI_* de core/icons_tabler.h (Tabler Icons cargado automaticamente "
"por fn::run_app via icon_font.cpp).");
section("Tabler icon set");
struct { const char* id; const char* glyph; const char* tip; } ic[] = {
{"##rl", TI_REFRESH, "Reload"},
{"##ad", TI_PLUS, "Add"},
{"##dl", TI_TRASH, "Delete"},
{"##dn", TI_CHEVRON_DOWN, "Dropdown"},
{"##cf", TI_SETTINGS, "Settings"},
{"##ok", TI_CHECK, "Check"},
{"##cl", TI_X, "Close"},
{"##ed", TI_PENCIL, "Edit"},
{"##sv", TI_DEVICE_FLOPPY, "Save"},
{"##sr", TI_SEARCH, "Search"},
{"##hp", TI_HELP, "Help"},
{"##hm", TI_HOME, "Home"},
};
for (auto& b : ic) {
icon_button(b.id, b.glyph, b.tip);
ImGui::SameLine();
}
ImGui::NewLine();
code_block(
"#include \"core/icons_tabler.h\"\n\n"
"if (icon_button(\"##reload\", TI_REFRESH, \"Reload\"))\n"
" reload_data();\n\n"
"// Mas de 5000 iconos disponibles — ver core/icons_tabler.h"
);
}
// ---------------------------------------------------------------------------
// toolbar
// ---------------------------------------------------------------------------
void demo_toolbar() {
demo_header("toolbar", "v1.0.0",
"Grupo horizontal con spacing consistente y separadores verticales sutiles. "
"El caller usa ImGui::SameLine entre items y toolbar_separator entre grupos.");
section("Example with two groups");
toolbar_begin();
button(TI_PLUS " New", V::Primary); ImGui::SameLine();
button(TI_FOLDER_OPEN " Open", V::Secondary); ImGui::SameLine();
button(TI_DEVICE_FLOPPY " Save",V::Secondary);
toolbar_separator();
icon_button("##set", TI_SETTINGS, "Settings");
ImGui::SameLine();
icon_button("##help", TI_HELP, "Help");
toolbar_end();
code_block(
"#include \"core/icons_tabler.h\"\n\n"
"toolbar_begin();\n"
" button(TI_PLUS \" New\", V::Primary); ImGui::SameLine();\n"
" button(TI_FOLDER_OPEN \" Open\", V::Secondary);\n"
" toolbar_separator();\n"
" icon_button(\"##set\", TI_SETTINGS, \"Settings\");\n"
"toolbar_end();"
);
}
// ---------------------------------------------------------------------------
// modal_dialog
// ---------------------------------------------------------------------------
void demo_modal() {
demo_header("modal_dialog", "v1.0.0",
"Popup modal centrada con estilo surface+border. Close con Escape o click en X. "
"Patron begin/end — modal_dialog_end debe llamarse siempre.");
static bool show = false;
if (button("Open modal", V::Primary)) show = true;
if (modal_dialog_begin("Demo modal", &show, ImVec2(380, 0))) {
ImGui::TextWrapped(
"Modal centrada en el viewport principal, con estilo tokens.");
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm));
static char buf[64] = {};
text_input("Name", buf, sizeof(buf), "escribe algo");
ImGui::Separator();
if (button("Cancel", V::Subtle)) show = false;
ImGui::SameLine();
if (button("Done", V::Primary)) show = false;
}
modal_dialog_end();
code_block(
"static bool show = false;\n"
"if (button(\"Open\", Primary)) show = true;\n"
"if (modal_dialog_begin(\"Title\", &show, ImVec2(380,0))) {\n"
" // ... campos del form ...\n"
" if (button(\"Done\", Primary)) show = false;\n"
"}\n"
"modal_dialog_end();"
);
}
// ---------------------------------------------------------------------------
// text_input
// ---------------------------------------------------------------------------
void demo_text_input() {
demo_header("text_input", "v1.0.0",
"Label muted + input estilizado con tokens. Full-width dentro del contenedor. "
"Placeholder opcional mostrado en text_dim cuando el buffer esta vacio.");
static char name[128] = {};
static char desc[256] = {};
static char tags[128] = {};
ImGui::BeginChild("##ti_wrap", ImVec2(420, 0), ImGuiChildFlags_AutoResizeY);
text_input("Name", name, sizeof(name), "my-new-thing");
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
text_input("Description", desc, sizeof(desc));
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
text_input("Tags (CSV)", tags, sizeof(tags), "imgui,ui,form");
ImGui::EndChild();
code_block(
"static char name[128] = {};\n"
"text_input(\"Name\", name, sizeof(name), \"my-new-thing\");\n"
"// true on change — se usa mas para validar en vivo\n"
"// que para leer el valor (que vive en el buffer)."
);
}
// ---------------------------------------------------------------------------
// select
// ---------------------------------------------------------------------------
void demo_select() {
demo_header("select", "v1.0.0",
"Dropdown con label muted y opcion (none) opcional. Mismo estilo tokens que text_input.");
static int lang_idx = 0;
static int domain_idx = -1;
const char* langs[] = {"go", "py", "ts", "sh", "cpp"};
const char* domains[] = {"core", "infra", "finance", "datascience", "viz"};
ImGui::BeginChild("##sl_wrap", ImVec2(420, 0), ImGuiChildFlags_AutoResizeY);
select("Language", &lang_idx, langs, 5);
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
select("Domain (optional)", &domain_idx, domains, 5, true);
ImGui::EndChild();
code_block(
"static int lang = 0;\n"
"const char* langs[] = {\"go\",\"py\",\"ts\",\"sh\",\"cpp\"};\n"
"select(\"Language\", &lang, langs, 5);"
);
}
// ---------------------------------------------------------------------------
// toast + inbox
// ---------------------------------------------------------------------------
void demo_toast() {
demo_header("toast", "v1.1.0",
"Notificaciones efimeras (~3.5s con fade-out) + inbox con campana. "
"La campana muestra badge con no-leidos y popover con las ultimas 50.");
section("Trigger toasts");
if (button("Info", V::Secondary)) toast_push(ToastKind::Info, "this is an info toast");
ImGui::SameLine();
if (button("Success", V::Primary)) toast_push(ToastKind::Success, "operation completed");
ImGui::SameLine();
if (button("Warning", V::Secondary)) toast_push(ToastKind::Warning, "heads up about something");
ImGui::SameLine();
if (button("Error", V::Danger)) toast_push(ToastKind::Error, "operation failed: reason");
section("Inbox (bell with unread badge)");
toast_inbox_button("##inbox_demo");
code_block(
"toast_push(ToastKind::Success, \"Reindexed 891 functions\");\n"
"toast_push(ToastKind::Error, \"HTTP 503: server down\");\n\n"
"// En la toolbar:\n"
"toast_inbox_button(\"##inbox\");\n\n"
"// Una vez por frame al final del render:\n"
"toast_render();"
);
}
// ---------------------------------------------------------------------------
// tree_view
// ---------------------------------------------------------------------------
void demo_tree_view() {
demo_header("tree_view", "v1.0.0",
"Tree low-level para jerarquias (ej. projects -> apps/analysis/vaults). "
"Sin estado interno: el caller gestiona seleccion y pasa 'selected' por parametro.");
static std::string selected;
section("Projects (fake)");
ImGui::BeginChild("##tv", ImVec2(360, 200), ImGuiChildFlags_Borders);
struct FakeProject { const char* id; const char* name; const char* apps[3]; };
const FakeProject projs[] = {
{"app_turismo", "app_turismo", {"guide_es", "offline_maps", nullptr}},
{"element_agents", "element_agents", {"matrix_bot", nullptr, nullptr}},
{"fn_monitoring", "fn_monitoring", {"sqlite_api", "registry_dashboard", nullptr}},
};
for (auto& p : projs) {
bool sel = (selected == p.id);
if (tree_branch_begin(p.id, p.name, sel)) {
if (tree_node_clicked()) selected = p.id;
for (int i = 0; i < 3 && p.apps[i]; i++) {
bool asel = (selected == p.apps[i]);
tree_leaf(p.apps[i], p.apps[i], asel);
if (tree_node_clicked()) selected = p.apps[i];
}
tree_branch_end();
} else if (tree_node_clicked()) {
selected = p.id;
}
}
ImGui::EndChild();
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
ImGui::Text("Selected: %s", selected.empty() ? "(none)" : selected.c_str());
ImGui::PopStyleColor();
code_block(
"static std::string sel;\n"
"if (tree_branch_begin(p.id, p.name, sel == p.id)) {\n"
" if (tree_node_clicked()) sel = p.id;\n"
" for (auto& a : p.apps) {\n"
" tree_leaf(a.id, a.name, sel == a.id);\n"
" if (tree_node_clicked()) sel = a.id;\n"
" }\n"
" tree_branch_end();\n"
"}"
);
}
// ---------------------------------------------------------------------------
// kpi_card
// ---------------------------------------------------------------------------
void demo_kpi_card() {
demo_header("kpi_card", "v1.3.0",
"Card compacta 86px con icono opcional + label muted, valor x1.4, trend con "
"TI_TRENDING_UP/DOWN y sparkline. Usa tokens: surface bg, border, radius md.");
if (ImGui::BeginTable("##kpi_grid", 4, ImGuiTableFlags_SizingStretchSame)) {
float history[] = {10, 12, 11, 15, 18, 17, 20};
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); kpi_card("Revenue", 20000.0f, 12.5f, history, 7, "$%.0f", TI_CASH);
ImGui::TableSetColumnIndex(1); kpi_card("Users", 1250.0f, 3.4f, history, 7, "%.0f", TI_USERS);
ImGui::TableSetColumnIndex(2); kpi_card("Churn", 2.1f, -0.3f, history, 7, "%.1f%%", TI_CHART_BAR);
ImGui::TableSetColumnIndex(3); kpi_card("Errors", 0.0f, 0.0f, nullptr, 0, "%.0f", TI_ALERT_CIRCLE);
ImGui::EndTable();
}
code_block(
"#include \"core/icons_tabler.h\"\n\n"
"float history[] = {10,12,11,15,18,17,20};\n"
"kpi_card(\"Revenue\", 20000.0f, 12.5f, history, 7, \"$%.0f\", TI_CASH);\n"
"kpi_card(\"Users\", 1250.0f, 3.4f, history, 7, \"%.0f\", TI_USERS);\n"
"// Sin delta ni history: muestra TI_MINUS como placeholder\n"
"kpi_card(\"Errors\", 0.0f, 0.0f, nullptr, 0, \"%.0f\", TI_ALERT_CIRCLE);"
);
}
// ---------------------------------------------------------------------------
// badge
// ---------------------------------------------------------------------------
void demo_badge() {
demo_header("badge", "v1.0.0",
"Etiqueta inline con 6 variantes semanticas. Equivalente a <Badge> de fn_library.");
section("Variants");
badge("Default", BadgeVariant::Default); ImGui::SameLine();
badge("Success", BadgeVariant::Success); ImGui::SameLine();
badge("Warning", BadgeVariant::Warning); ImGui::SameLine();
badge("Error", BadgeVariant::Error); ImGui::SameLine();
badge("Info", BadgeVariant::Info); ImGui::SameLine();
badge("Outline", BadgeVariant::Outline);
section("In context (table row)");
ImGui::Text("filter_slice_go_core"); ImGui::SameLine();
badge("pure", BadgeVariant::Success); ImGui::SameLine();
badge("tested", BadgeVariant::Info);
code_block(
"badge(\"pure\", BadgeVariant::Success);\n"
"badge(\"stale\", BadgeVariant::Warning);\n"
"badge(\"broken\", BadgeVariant::Error);"
);
}
// ---------------------------------------------------------------------------
// empty_state
// ---------------------------------------------------------------------------
void demo_empty_state() {
demo_header("empty_state", "v1.0.0",
"Icono grande muted + titulo + descripcion opcional. Para listas/tablas vacias.");
ImGui::BeginChild("##es", ImVec2(0, 180), ImGuiChildFlags_Borders);
empty_state("( no data )", "No projects yet",
"Create one under projects/{name}/ with project.md and reindex");
ImGui::EndChild();
code_block(
"if (apps.empty()) {\n"
" empty_state(\"( no data )\", \"No apps yet\",\n"
" \"Click + Add to create one\");\n"
" return;\n"
"}"
);
}
// ---------------------------------------------------------------------------
// page_header
// ---------------------------------------------------------------------------
void demo_page_header() {
demo_header("page_header", "v1.0.0",
"Header de pagina con titulo, subtitulo opcional y separador final. "
"Patron begin/end permite insertar acciones entre titulo y separador.");
page_header_begin("Dashboard", "13 apps, 3 projects, 2 analyses");
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 140.0f);
toolbar_begin();
button("Reload", V::Subtle); ImGui::SameLine();
button("+ Add", V::Secondary);
toolbar_end();
page_header_end();
code_block(
"page_header_begin(\"Dashboard\", subtitle);\n"
"ImGui::SameLine(ImGui::GetContentRegionAvail().x - 140);\n"
"toolbar_begin();\n"
" button(\"Reload\", Subtle);\n"
"toolbar_end();\n"
"page_header_end();"
);
}
// ---------------------------------------------------------------------------
// dashboard_panel
// ---------------------------------------------------------------------------
void demo_dashboard_panel() {
demo_header("dashboard_panel", "v1.0.0",
"Contenedor tipo panel con titulo, bordes redondeados, bg surface. "
"Auto-resize-Y segun contenido. Usa min_width/min_height como piso.");
if (dashboard_panel_begin("Revenue", 0, 120.0f)) {
ImGui::Text("Some panel content goes here.");
ImGui::Text("Anything drawn inside lives in the child window.");
}
dashboard_panel_end();
code_block(
"if (dashboard_panel_begin(\"Revenue\", 0, 120.0f)) {\n"
" ImGui::Text(\"content\");\n"
"}\n"
"dashboard_panel_end();"
);
}
} // namespace gallery
+215
View File
@@ -0,0 +1,215 @@
// Demos faltantes: process_runner (Core), candlestick / gauge / heatmap /
// table_view (Viz). Aniade cobertura sobre los primitivos del registry que
// no tenian su entry en la gallery.
#include "demos.h"
#include "demo.h"
#include "core/process_runner.h"
#include "viz/candlestick.h"
#include "viz/gauge.h"
#include "viz/heatmap.h"
#include "viz/table_view.h"
#include <imgui.h>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <thread>
#include <vector>
namespace gallery {
// ---------------------------------------------------------------------------
// process_runner (Core)
// ---------------------------------------------------------------------------
void demo_process_runner() {
demo_header("process_runner", "v1.0.0",
"Ejecuta una tarea en std::thread en background y expone estado thread-safe "
"(idle/running/success/error). El widget runner_status() dibuja inline un "
"spinner mientras corre y un mensaje de Success/Error al terminar.");
static fn_ui::ProcessRunner runner;
section("Tarea simulada (sleep 2s)");
{
if (ImGui::Button("Run task")) {
if (!runner.is_busy()) {
fn_ui::runner_trigger(runner, [](std::string& out) -> bool {
std::this_thread::sleep_for(std::chrono::seconds(2));
out = "task done in 2s";
return true;
});
}
}
ImGui::SameLine();
if (ImGui::Button("Run failing task")) {
if (!runner.is_busy()) {
fn_ui::runner_trigger(runner, [](std::string& out) -> bool {
std::this_thread::sleep_for(std::chrono::seconds(1));
out = "simulated failure";
return false;
});
}
}
ImGui::SameLine();
if (ImGui::Button("Reset")) runner.reset();
fn_ui::runner_status(runner, "Working...");
}
code_block(
"static fn_ui::ProcessRunner r;\n"
"if (button(\"Run\", Primary) && !r.is_busy()) {\n"
" fn_ui::runner_trigger(r, [](std::string& out) -> bool {\n"
" return do_work(&out);\n"
" });\n"
"}\n"
"fn_ui::runner_status(r, \"Working...\");"
);
}
// ---------------------------------------------------------------------------
// candlestick (Viz)
// ---------------------------------------------------------------------------
void demo_candlestick() {
demo_header("candlestick", "v1.0.0",
"Grafico de velas OHLC con ImPlot custom rendering. Verde si close >= open, "
"rojo si bajista. Tooltip al hover muestra OHLC del dia.");
section("OHLC sintetico (30 dias)");
{
static std::vector<double> dates, opens, closes, lows, highs;
if (dates.empty()) {
dates.reserve(30); opens.reserve(30); closes.reserve(30);
lows.reserve(30); highs.reserve(30);
double price = 100.0;
for (int i = 0; i < 30; ++i) {
double drift = std::sin(i * 0.4) * 1.2;
double o = price;
double c = price + drift + (i % 3 == 0 ? -0.6 : 0.4);
double l = std::min(o, c) - 0.8 - (i % 5) * 0.1;
double h = std::max(o, c) + 0.8 + (i % 4) * 0.1;
dates.push_back(double(i));
opens.push_back(o);
closes.push_back(c);
lows.push_back(l);
highs.push_back(h);
price = c;
}
}
candlestick("##cs", dates.data(), opens.data(), closes.data(),
lows.data(), highs.data(), int(dates.size()));
}
code_block(
"candlestick(\"##cs\", dates, opens, closes, lows, highs, n,\n"
" /*width_percent=*/0.25f, /*tooltip=*/true);"
);
}
// ---------------------------------------------------------------------------
// gauge (Viz)
// ---------------------------------------------------------------------------
void demo_gauge() {
demo_header("gauge", "v1.0.0",
"Indicador circular tipo velocimetro con ImGui DrawList. Color interpolado "
"verde -> amarillo -> rojo segun el valor normalizado.");
static float v_cpu = 0.32f, v_mem = 0.78f, v_gpu = 0.55f;
section("Tres gauges con sliders");
{
ImGui::SliderFloat("cpu", &v_cpu, 0.0f, 1.0f);
ImGui::SliderFloat("mem", &v_mem, 0.0f, 1.0f);
ImGui::SliderFloat("gpu", &v_gpu, 0.0f, 1.0f);
ImGui::Spacing();
ImGui::BeginGroup();
gauge("CPU", v_cpu, 0.0f, 1.0f, 60.0f);
ImGui::EndGroup();
ImGui::SameLine(0.0f, 24.0f);
ImGui::BeginGroup();
gauge("MEM", v_mem, 0.0f, 1.0f, 60.0f);
ImGui::EndGroup();
ImGui::SameLine(0.0f, 24.0f);
ImGui::BeginGroup();
gauge("GPU", v_gpu, 0.0f, 1.0f, 60.0f);
ImGui::EndGroup();
}
code_block("gauge(\"CPU\", 0.32f, 0.0f, 1.0f, 60.0f);");
}
// ---------------------------------------------------------------------------
// heatmap (Viz)
// ---------------------------------------------------------------------------
void demo_heatmap() {
demo_header("heatmap", "v1.0.0",
"Mapa de calor 2D con ImPlot. Datos row-major. Util para correlation "
"matrices, attention maps, distribuciones 2D discretas.");
constexpr int R = 12;
constexpr int C = 12;
static float values[R * C] = {0};
static bool init = false;
if (!init) {
for (int r = 0; r < R; ++r) {
for (int c = 0; c < C; ++c) {
float dx = (c - C * 0.5f) / float(C);
float dy = (r - R * 0.5f) / float(R);
values[r * C + c] = std::exp(-(dx * dx + dy * dy) * 6.0f);
}
}
init = true;
}
section("Gaussian 12x12");
{
heatmap("##hm", values, R, C, 0.0f, 1.0f);
}
code_block(
"float values[R * C];\n"
"// fill row-major: values[r * C + c] = ...\n"
"heatmap(\"##hm\", values, R, C, /*min=*/0.0f, /*max=*/1.0f);"
);
}
// ---------------------------------------------------------------------------
// table_view (Viz)
// ---------------------------------------------------------------------------
void demo_table_view() {
demo_header("table_view", "v1.0.0",
"Tabla interactiva con sorting indicators y scroll usando la ImGui Tables API. "
"Headers + cells row-major. Util para dashboards y inspectores.");
section("Lenguajes del registry");
{
const char* headers[] = {"id", "lang", "domain", "purity"};
// 6 filas x 4 cols, row-major
const char* cells[] = {
"filter_slice_go_core", "go", "core", "pure",
"metabase_setup_py_infra", "py", "infra", "impure",
"rsync_deploy_bash_infra", "sh", "infra", "impure",
"button_cpp_core", "cpp", "core", "pure",
"gl_texture_load_cpp_gfx", "cpp", "gfx", "impure",
"audio_fft_cpp_core", "cpp", "core", "pure",
};
const int row_count = 6;
const int col_count = 4;
table_view("##tbl", headers, col_count, cells, row_count);
}
code_block(
"const char* headers[] = {\"id\", \"lang\", \"domain\"};\n"
"const char* cells[] = {/* row-major: r0c0,r0c1,r0c2, r1c0,... */};\n"
"table_view(\"##tbl\", headers, 3, cells, n_rows);"
);
}
} // namespace gallery
+196
View File
@@ -0,0 +1,196 @@
// Demos del dominio gfx — primitivos OpenGL/shader que viven en
// cpp/functions/gfx/. La pieza distintiva de shaders_lab es el
// shader_canvas: framebuffer + fullscreen quad + programa GL animado por
// time/resolution/mouse.
#include "demos.h"
#include "demo.h"
#include "gfx/shader_canvas.h"
#include "gfx/gl_shader.h"
#include "gfx/gl_loader.h"
#include <imgui.h>
#include <chrono>
namespace gallery {
namespace {
// Fragment shader sintetico — gradiente animado con celdas. Usa los uniforms
// estandar que compile_fragment inyecta: u_resolution, u_time, u_mouse.
const char* kShaderSrc = R"(
void mainImage() {
vec2 uv = gl_FragCoord.xy / u_resolution;
vec2 cell = uv * 8.0;
vec2 ipos = floor(cell);
vec2 fpos = fract(cell) - 0.5;
float t = u_time * 0.6;
float wave = sin(ipos.x * 0.7 + ipos.y * 0.5 + t);
float dist = length(fpos);
vec3 a = vec3(0.30, 0.43, 0.96); // indigo
vec3 b = vec3(0.95, 0.45, 0.85); // pink
vec3 col = mix(a, b, 0.5 + 0.5 * wave);
// Mouse focus: oscurecemos celdas lejanas al cursor.
vec2 m = u_mouse / u_resolution;
float fm = 1.0 - smoothstep(0.0, 0.6, length(uv - m));
col *= 0.6 + 0.4 * fm;
// Disco interior por celda con borde suave.
col *= smoothstep(0.5, 0.45, dist);
fragColor = vec4(col, 1.0);
}
void main() {
mainImage();
}
)";
struct CanvasState {
fn::gfx::ShaderCanvas canvas;
bool compiled = false;
bool compile_failed = false;
std::string err_msg;
std::chrono::steady_clock::time_point t0;
};
CanvasState& state() {
static CanvasState s;
return s;
}
} // namespace
void demo_shader_canvas() {
demo_header("shader_canvas", "v1.0.0",
"Framebuffer + fullscreen quad + shader GLSL animado. La misma pieza "
"que usa shaders_lab para el preview en vivo. Uniforms u_time / u_resolution / u_mouse "
"los inyecta gl_shader::compile_fragment automaticamente.");
auto& s = state();
// Compilacion lazy (en el primer frame ya hay contexto GL valido).
if (!s.compiled && !s.compile_failed) {
fn::gfx::gl_loader_init();
fn::gfx::canvas_init(s.canvas);
auto cr = fn::gfx::compile_fragment(kShaderSrc);
if (!cr.ok) {
s.compile_failed = true;
s.err_msg = cr.err_msg;
} else {
fn::gfx::canvas_set_program(s.canvas, cr.program);
s.t0 = std::chrono::steady_clock::now();
s.compiled = true;
}
}
if (s.compile_failed) {
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1),
"Compilacion del fragment shader fallo:\n%s",
s.err_msg.c_str());
return;
}
section("Live preview");
// Render del shader en un panel ~480x300 px. canvas_render hace resize
// automatico segun GetContentRegionAvail si lo dejas crecer.
ImGui::BeginChild("##shader_preview", ImVec2(480, 300),
ImGuiChildFlags_Borders);
const float dt = std::chrono::duration<float>(
std::chrono::steady_clock::now() - s.t0).count();
fn::gfx::canvas_render(s.canvas, dt);
ImGui::EndChild();
code_block(
"#include \"gfx/shader_canvas.h\"\n"
"#include \"gfx/gl_shader.h\"\n\n"
"static fn::gfx::ShaderCanvas canvas;\n"
"// Setup (una vez):\n"
"fn::gfx::canvas_init(canvas);\n"
"auto cr = fn::gfx::compile_fragment(user_glsl);\n"
"if (cr.ok) fn::gfx::canvas_set_program(canvas, cr.program);\n\n"
"// Cada frame, dentro de un Begin/End:\n"
"fn::gfx::canvas_render(canvas, time_seconds);"
);
}
// Issue 0049b — Mostrar la version de OpenGL del contexto y un puñado de
// limites 4.3 que confirman que compute shaders / SSBO / image load-store
// estan disponibles. No es codigo del registry, solo introspeccion del
// driver — sin estado, sin side effects: solo glGetString + glGetIntegerv.
#ifndef GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS
#define GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS 0x90DD
#endif
#ifndef GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS
#define GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS 0x90DC
#endif
#ifndef GL_MAX_COMPUTE_SHARED_MEMORY_SIZE
#define GL_MAX_COMPUTE_SHARED_MEMORY_SIZE 0x8262
#endif
void demo_gl_info() {
demo_header("gl_info", "v1.0.0",
"Introspeccion del contexto OpenGL activo (issue 0049b). El framework "
"ahora pide GL 4.3 core, lo que habilita compute shaders, SSBOs, image "
"load/store y atomic counters — bloques esenciales del graph_renderer "
"GPU del proyecto osint_graph.");
auto gl_str = [](GLenum e) -> const char* {
const GLubyte* s = glGetString(e);
return s ? reinterpret_cast<const char*>(s) : "(null)";
};
section("Driver");
ImGui::Text("Vendor: %s", gl_str(GL_VENDOR));
ImGui::Text("Renderer: %s", gl_str(GL_RENDERER));
ImGui::Text("Version: %s", gl_str(GL_VERSION));
ImGui::Text("GLSL: %s", gl_str(GL_SHADING_LANGUAGE_VERSION));
GLint major = 0, minor = 0;
glGetIntegerv(GL_MAJOR_VERSION, &major);
glGetIntegerv(GL_MINOR_VERSION, &minor);
const bool has_43 = (major > 4) || (major == 4 && minor >= 3);
section("Capabilities");
ImGui::Text("Context: %d.%d core", major, minor);
if (has_43) {
ImGui::TextColored(ImVec4(0.40f, 0.85f, 0.40f, 1.0f),
"OpenGL 4.3+ — compute shaders, SSBOs, image load/store, atomic counters: AVAILABLE");
} else {
ImGui::TextColored(ImVec4(0.95f, 0.55f, 0.30f, 1.0f),
"OpenGL < 4.3 — compute shaders / SSBOs NOT available on this driver");
}
section("Limits");
GLint v = 0;
auto row = [&](const char* label, GLenum e) {
v = 0;
glGetIntegerv(e, &v);
ImGui::Text("%-44s %d", label, v);
};
row("GL_MAX_TEXTURE_SIZE", GL_MAX_TEXTURE_SIZE);
row("GL_MAX_VERTEX_ATTRIBS", GL_MAX_VERTEX_ATTRIBS);
row("GL_MAX_UNIFORM_BLOCK_SIZE", GL_MAX_UNIFORM_BLOCK_SIZE);
if (has_43) {
row("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS);
row("GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS);
row("GL_MAX_COMPUTE_SHARED_MEMORY_SIZE", GL_MAX_COMPUTE_SHARED_MEMORY_SIZE);
}
code_block(
"// Solo glGetString + glGetIntegerv — sin loader extra.\n"
"GLint major = 0, minor = 0;\n"
"glGetIntegerv(GL_MAJOR_VERSION, &major);\n"
"glGetIntegerv(GL_MINOR_VERSION, &minor);\n"
"bool has_compute = (major > 4) || (major == 4 && minor >= 3);"
);
}
} // namespace gallery
+127
View File
@@ -0,0 +1,127 @@
// Demo de gl_texture_load (cpp/functions/gfx/gl_texture_load.{h,cpp}).
// Carga assets/sample.png y lo muestra con ImGui::Image. Sliders para tint
// RGB que se aplican como modulacion (ImGui::Image acepta tint_col).
//
// Limitacion: el "zoom UV" se simula moviendo uv0/uv1 (que ImGui::Image acepta
// nativamente). Asi evitamos compilar un shader custom adicional para la demo.
#include "demos.h"
#include "demo.h"
#include "gfx/gl_texture_load.h"
#include "gfx/gl_loader.h"
#include <imgui.h>
#include <cstdio>
#include <cstring>
namespace gallery {
namespace {
struct TextureState {
fn::GlTexture tex{};
bool tried_load = false;
std::string_view err;
char err_buf[256] = {0};
float tint[3] = {1.0f, 1.0f, 1.0f};
float zoom = 1.0f; // 1.0 = sin zoom; >1 hace UV mas pequeno
};
TextureState& state() {
static TextureState s;
return s;
}
// Resuelve un path para el asset. Probamos varios candidatos relativos al cwd
// del binario (puede lanzarse desde build/ o desde la raiz del repo).
const char* resolve_sample_path() {
static const char* candidates[] = {
"assets/sample.png",
"apps/primitives_gallery/assets/sample.png",
"cpp/apps/primitives_gallery/assets/sample.png",
"../cpp/apps/primitives_gallery/assets/sample.png",
"../../cpp/apps/primitives_gallery/assets/sample.png",
"../../../cpp/apps/primitives_gallery/assets/sample.png",
nullptr,
};
for (int i = 0; candidates[i]; i++) {
FILE* f = std::fopen(candidates[i], "rb");
if (f) { std::fclose(f); return candidates[i]; }
}
return candidates[0]; // devolver el primer candidato para que el error sea mas descriptivo
}
} // namespace
void demo_gl_texture() {
demo_header("gl_texture_load", "v1.0.0",
"Carga PNG/JPG/HDR desde disco a una textura GL lista para sampler2D. "
"Vendorea stb_image (cpp/vendor/stb/). Demo: assets/sample.png "
"(damero 256x256), tint RGB modulando ImGui::Image, zoom UV.");
auto& s = state();
if (!s.tried_load) {
// Asegurar simbolos GL resueltos (Linux no-op, Windows wglGetProcAddress).
fn::gfx::gl_loader_init();
const char* path = resolve_sample_path();
s.tex = fn::gl_texture_load(path, /*flip_y=*/true, /*srgb=*/false);
if (!s.tex.ok()) {
std::snprintf(s.err_buf, sizeof(s.err_buf),
"no se pudo cargar '%s': %s",
path, fn::gl_texture_last_error());
}
s.tried_load = true;
}
if (!s.tex.ok()) {
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", s.err_buf);
ImGui::TextWrapped(
"El binario busca el PNG en varios paths relativos al cwd. "
"Lanzar desde la raiz del repo o desde cpp/build/ deberia funcionar.");
return;
}
section("Texture info");
ImGui::Text("size: %d x %d px", s.tex.w, s.tex.h);
ImGui::Text("channels: %d (forzado a RGBA en upload)", s.tex.channels);
ImGui::Text("gl_id: %u", (unsigned)s.tex.id);
section("Tint + zoom");
ImGui::SliderFloat3("tint RGB", s.tint, 0.0f, 2.0f, "%.2f");
ImGui::SliderFloat("zoom UV", &s.zoom, 0.25f, 4.0f, "%.2fx");
section("Preview");
// Calcular UVs centradas con zoom: 1.0 = (0,0)-(1,1), 2.0 = (0.25,0.25)-(0.75,0.75)
float u_half = 0.5f / (s.zoom > 0.001f ? s.zoom : 0.001f);
ImVec2 uv0(0.5f - u_half, 0.5f - u_half);
ImVec2 uv1(0.5f + u_half, 0.5f + u_half);
ImVec4 tint(s.tint[0], s.tint[1], s.tint[2], 1.0f);
// Conversion GLuint -> ImTextureID. ImGui::Image acepta cualquier id de
// textura del backend; en imgui_impl_opengl3 es directamente el GLuint.
ImTextureID tid = (ImTextureID)(intptr_t)s.tex.id;
ImGui::ImageWithBg(tid, ImVec2(384.0f, 384.0f), uv0, uv1,
ImVec4(0, 0, 0, 0), tint);
code_block(
"#include \"gfx/gl_texture_load.h\"\n\n"
"auto tex = fn::gl_texture_load(\"assets/sample.png\");\n"
"if (!tex.ok()) {\n"
" fprintf(stderr, \"%s\\n\", fn::gl_texture_last_error());\n"
" return 1;\n"
"}\n"
"// uso en shader:\n"
"glUseProgram(prog);\n"
"fn::gl_texture_bind_uniform(prog, \"u_tex\", tex, /*unit=*/0);\n"
"glDrawArrays(GL_TRIANGLES, 0, 6);\n\n"
"// o en ImGui directamente:\n"
"ImGui::Image((ImTextureID)(intptr_t)tex.id, ImVec2(w, h));"
);
}
} // namespace gallery
+443
View File
@@ -0,0 +1,443 @@
#include "demos.h"
#include "demo.h"
#include "viz/graph_types.h"
#include "viz/graph_viewport.h"
#include "viz/graph_force_layout.h"
#include "viz/graph_force_layout_gpu.h"
#include "viz/graph_layouts.h"
#include "viz/graph_labels.h"
#include "core/button.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cmath>
#include <cstdio>
#include <vector>
namespace gallery {
// Paleta del demo: 8 colores tipo Mantine. v2.0 los usamos a traves de la
// tabla EntityType en lugar de escribirlos por nodo. Asi el modelo nuevo
// queda demostrado tal cual lo van a usar las apps reales (osint_graph,
// fn_explorer): tabla pequena de tipos + nodos que solo guardan type_id.
static const uint32_t k_demo_palette[] = {
0xFFEF8D5Bu, 0xFF8CCA58u, 0xFF3E97F5u, 0xFF5051D9u,
0xFFE07FB8u, 0xFFCCCD5Fu, 0xFF52CDF2u, 0xFF61D199u,
};
static constexpr int k_demo_palette_n =
sizeof(k_demo_palette) / sizeof(k_demo_palette[0]);
// Tabla compartida entre regeneraciones — las apariencias no cambian aunque
// el usuario regenere el grafo, asi que vive como `static`.
static EntityType s_demo_entity_types[k_demo_palette_n];
static RelationType s_demo_relation_types[1];
static bool s_demo_types_initialized = false;
static void init_demo_types() {
if (s_demo_types_initialized) return;
for (int k = 0; k < k_demo_palette_n; ++k) {
s_demo_entity_types[k] = entity_type(k_demo_palette[k],
SHAPE_CIRCLE, 4.0f, "cluster");
}
s_demo_relation_types[0] = relation_type(0xFF888888u, EDGE_SOLID, 1.0f, "default");
s_demo_types_initialized = true;
}
// Genera un grafo sintetico con N nodos en K clusters. Cada nodo tiene
// `edges_per_node` aristas intra-cluster + un pct% global inter-cluster.
// Cluster radio escala con sqrt(N) para que la "nube" no sea siempre el
// mismo cuadrado de 200 px — a 1M nodos crece a ~6 km de radio en graph
// space y los nodos pueden esparcirse libremente sin caja artificial.
static void generate_synthetic_graph(int N, int K,
int edges_per_node, int inter_pct,
std::vector<GraphNode>& nodes_out,
std::vector<GraphEdge>& edges_out) {
nodes_out.clear();
edges_out.clear();
nodes_out.reserve(N);
edges_out.reserve((size_t)N * (size_t)edges_per_node + (size_t)N * (size_t)inter_pct / 100u);
unsigned seed = 0x1234abcd;
auto rnd = [&]() {
seed = seed * 1664525u + 1013904223u;
return static_cast<float>((seed >> 8) & 0xffffff) / 16777216.0f;
};
// Cluster radius y scatter escalan con sqrt(N) para que los nodos no
// queden empaquetados al subir el slider. A 1M nodes el espacio inicial
// es ~12k px de lado en lugar de los 280 px hardcoded de antes.
const float scale = std::sqrt(static_cast<float>(std::max(N, 1)));
const float cluster_r = 12.0f * scale;
const float scatter = 4.0f * scale;
std::vector<float> cluster_cx(K), cluster_cy(K);
for (int k = 0; k < K; k++) {
float angle = 2.0f * 3.14159f * k / K;
cluster_cx[k] = std::cos(angle) * cluster_r;
cluster_cy[k] = std::sin(angle) * cluster_r;
}
for (int i = 0; i < N; i++) {
int k = i % K;
// type_id mapea al EntityType (k % k_demo_palette_n) que define
// color y shape. size_override = 3..5 px para conservar la
// variacion sutil del demo v1 — apariencia visual identica.
uint16_t tid = static_cast<uint16_t>(k % k_demo_palette_n);
GraphNode n = graph_node(
cluster_cx[k] + (rnd() - 0.5f) * scatter,
cluster_cy[k] + (rnd() - 0.5f) * scatter,
tid);
n.size_override = 3.0f + rnd() * 2.0f;
n.user_data = static_cast<uint64_t>(i);
nodes_out.push_back(n);
}
auto add_edge = [&](uint32_t a, uint32_t b, float w) {
if (a == b) return;
edges_out.push_back(graph_edge(a, b, w));
};
int per_cluster = N / K;
for (int k = 0; k < K; k++) {
int base = k * per_cluster;
int end = (k == K - 1) ? N : (base + per_cluster);
int size = end - base;
if (size < 2) continue;
for (int i = base; i < end; i++) {
for (int e = 0; e < edges_per_node; e++) {
int j = base + static_cast<int>(rnd() * size);
add_edge(static_cast<uint32_t>(i),
static_cast<uint32_t>(j), 1.0f);
}
}
}
// Inter-cluster: pct% del total de nodos
long long inter = (long long)N * (long long)inter_pct / 100LL;
for (long long e = 0; e < inter; e++) {
uint32_t a = static_cast<uint32_t>(rnd() * N);
uint32_t b = static_cast<uint32_t>(rnd() * N);
add_edge(a, b, 0.3f);
}
}
void demo_graph() {
demo_header("graph_viewport", "v1.0.0",
"Pipeline completo de visualizacion de grafos: graph_renderer (instanced GPU) "
"+ graph_force_layout (Barnes-Hut) + graph_spatial_hash (hit-testing). "
"Render a FBO mostrado via ImGui::Image — escala a decenas de miles de nodos.");
static int s_n_nodes = 1000;
static int s_n_clusters = 6;
static int s_edges_per_n = 3; // aristas intra-cluster por nodo
static int s_inter_pct = 5; // % de nodos para edges inter-cluster
static float s_repulsion = 3500.0f; // fuerza de dispersion entre nodos
static float s_attraction = 0.02f; // muelle entre nodos conectados
static float s_gravity = 0.001f; // tiron hacia el centro
static std::vector<GraphNode> s_nodes;
static std::vector<GraphEdge> s_edges;
static GraphData s_graph{};
static GraphViewportState s_state;
static bool s_initialized = false;
static bool s_needs_regen = true;
// GPU layout (issue 0049h): toggle CPU/GPU. ctx se crea perezosamente al
// primer frame en GPU mode; max_nodes/max_edges se dimensionan al maximo
// que ofrece el slider (1M nodos x 10 edges/nodo = 10M edges) — los SSBOs
// ocupan ~80 MB en ese tope, suficientemente barato para no
// recrear el ctx cada Regenerate. Si compute no esta disponible, el
// toggle queda deshabilitado.
static bool s_use_gpu = false;
static ForceLayoutGPU* s_gpu_ctx = nullptr;
static bool s_gpu_dirty = true; // re-upload tras regen / cambio
// Layout estatico activo (issue 0049i). 0=force (iterativo), 1=grid,
// 2=circular, 3=radial, 4=hierarchical, 5=fixed.
static int s_layout_mode = 0;
const char* k_layout_names[] = {
"force", "grid", "circular", "radial", "hierarchical", "fixed"
};
static int s_apply_layout = 0; // se incrementa cuando hay que reaplicar
// Labels (issue 0049j). LabelPolicy controlable desde la UI.
static graph::LabelPolicy s_label_policy;
static bool s_labels_enabled = true;
if (s_needs_regen) {
init_demo_types();
generate_synthetic_graph(s_n_nodes, s_n_clusters,
s_edges_per_n, s_inter_pct,
s_nodes, s_edges);
s_graph.nodes = s_nodes.data();
s_graph.node_count = static_cast<int>(s_nodes.size());
s_graph.node_capacity = static_cast<int>(s_nodes.capacity());
s_graph.edges = s_edges.data();
s_graph.edge_count = static_cast<int>(s_edges.size());
s_graph.edge_capacity = static_cast<int>(s_edges.capacity());
s_graph.types = s_demo_entity_types;
s_graph.type_count = k_demo_palette_n;
s_graph.rel_types = s_demo_relation_types;
s_graph.rel_type_count = 1;
s_graph.update_bounds();
s_state.layout_running = true;
s_state.layout_energy = 0.0f;
s_needs_regen = false;
s_initialized = true;
s_gpu_dirty = true;
}
section("Controls");
{
using namespace fn_ui;
ImGui::PushItemWidth(180);
// Slider Nodes con escala logaritmica para que sea util tanto a 100
// como a 1M sin tener que arrastrar 10000px.
ImGui::SliderInt("Nodes", &s_n_nodes, 100, 1000000, "%d",
ImGuiSliderFlags_Logarithmic);
ImGui::SameLine();
ImGui::SliderInt("Clusters", &s_n_clusters, 2, 16);
ImGui::SliderInt("Edges/node", &s_edges_per_n, 1, 10);
ImGui::SameLine();
ImGui::SliderInt("Inter %", &s_inter_pct, 0, 30, "%d%%");
ImGui::SliderFloat("Repulsion", &s_repulsion, 100.0f, 20000.0f, "%.0f");
ImGui::SameLine();
ImGui::SliderFloat("Attraction", &s_attraction, 0.001f, 0.5f, "%.3f");
ImGui::SameLine();
ImGui::SliderFloat("Gravity", &s_gravity, 0.0f, 0.05f, "%.4f");
ImGui::PopItemWidth();
if (button("Regenerate", ButtonVariant::Primary)) s_needs_regen = true;
ImGui::SameLine();
if (button(s_state.layout_running ? "Pause layout" : "Resume layout",
ButtonVariant::Secondary)) {
s_state.layout_running = !s_state.layout_running;
}
ImGui::SameLine();
if (button("Fit view", ButtonVariant::Subtle)) {
graph_viewport_fit(s_graph, s_state);
}
ImGui::SameLine();
// Toggle GPU layout. Si compute no esta disponible (Mesa software o
// driver < 4.3), deshabilitamos visualmente el checkbox.
bool prev_gpu = s_use_gpu;
if (s_gpu_ctx == nullptr && s_use_gpu == false) {
// primera oportunidad: intentar crear el ctx para detectar soporte.
// Lazy init solo si el usuario lo activa.
}
ImGui::Checkbox("GPU layout", &s_use_gpu);
if (s_use_gpu != prev_gpu) {
s_gpu_dirty = true; // re-upload al cambiar de modo
}
// Selector de layout (issue 0049i).
ImGui::PushItemWidth(140);
int prev_mode = s_layout_mode;
if (ImGui::Combo("Layout", &s_layout_mode,
k_layout_names, IM_ARRAYSIZE(k_layout_names))) {
// Cambio de modo: reaplicar instantaneamente
s_apply_layout++;
}
if (prev_mode != s_layout_mode) {
// En "force" volvemos a animar; en cualquier estatico paramos.
s_state.layout_running = (s_layout_mode == 0);
}
ImGui::PopItemWidth();
ImGui::SameLine();
if (button("Apply layout", ButtonVariant::Subtle)) s_apply_layout++;
// --- Labels (issue 0049j) ---------------------------------------
ImGui::Checkbox("Labels", &s_labels_enabled);
ImGui::SameLine();
ImGui::PushItemWidth(140);
ImGui::SliderInt("Max visible", &s_label_policy.max_visible, 0, 1000);
ImGui::SameLine();
ImGui::SliderFloat("Font", &s_label_policy.font_size,
8.0f, 24.0f, "%.0f");
ImGui::SameLine();
ImGui::SliderFloat("Min px", &s_label_policy.min_node_pixel_size,
0.0f, 40.0f, "%.0f");
ImGui::PopItemWidth();
ImGui::SameLine();
ImGui::Checkbox("Selected", &s_label_policy.always_for_selected);
ImGui::SameLine();
ImGui::Checkbox("Hovered", &s_label_policy.always_for_hovered);
ImGui::SameLine();
ImGui::Checkbox("Pinned", &s_label_policy.always_for_pinned);
}
section("Stats");
{
// Una sola linea fija — sin secciones condicionales que cambien la
// altura del panel (eso provocaba que el viewport saltara al hacer
// hover/select).
char hover_buf[32];
char sel_buf[32];
if (s_state.hovered_node >= 0) {
std::snprintf(hover_buf, sizeof(hover_buf), "#%d t%u",
s_state.hovered_node,
(unsigned)s_nodes[s_state.hovered_node].type_id);
} else {
std::snprintf(hover_buf, sizeof(hover_buf), "-");
}
if (s_state.selected_node >= 0) {
std::snprintf(sel_buf, sizeof(sel_buf), "#%d", s_state.selected_node);
} else {
std::snprintf(sel_buf, sizeof(sel_buf), "-");
}
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
ImGui::Text("nodes=%d edges=%d energy=%.2f fps=%.0f | hover=%s sel=%s",
s_graph.node_count, s_graph.edge_count,
s_state.layout_energy, ImGui::GetIO().Framerate,
hover_buf, sel_buf);
ImGui::PopStyleColor();
}
// Aplicar layout estatico cuando se solicita (cambio de modo / boton).
static int s_last_apply = -1;
if (s_apply_layout != s_last_apply) {
s_last_apply = s_apply_layout;
switch (s_layout_mode) {
case 1: graph::layout_grid (s_graph, 25.0f); break;
case 2: graph::layout_circular (s_graph, 200.0f); break;
case 3: graph::layout_radial (s_graph, 0, 80.0f); break;
case 4: graph::layout_hierarchical(s_graph, 0, 120.0f, 50.0f); break;
case 5: graph::layout_fixed (s_graph); break;
case 0: default:
// force: dejar las posiciones actuales; el bucle lo refinara
break;
}
s_gpu_dirty = true;
if (s_layout_mode != 0) graph_viewport_fit(s_graph, s_state);
}
section("Viewport (drag=pan, wheel=zoom, click=select, shift+drag=lasso, ctrl+click=toggle)");
if (s_initialized) {
// Avanzamos 1 paso de force layout cada frame mientras layout_running.
// Auto-pause: si la energia por nodo cae bajo el umbral durante N
// frames consecutivos, paramos la simulacion automaticamente — el
// grafo ya esta estable. El usuario lo retoma con "Resume layout"
// o "Regenerate".
static int s_low_energy_frames = 0;
const int k_pause_after_frames = 30;
const float k_pause_per_node = 0.001f; // umbral de energia/nodo
if (s_state.layout_running && s_layout_mode == 0) {
ForceLayoutConfig cfg;
cfg.repulsion = s_repulsion;
cfg.attraction = s_attraction;
cfg.gravity = s_gravity;
cfg.iterations = 1;
if (s_use_gpu) {
if (!s_gpu_ctx) {
s_gpu_ctx = graph_force_layout_gpu_create(s_graph.node_count + 1024,
s_graph.edge_count + 1024);
s_gpu_dirty = true;
}
if (s_gpu_ctx) {
if (s_gpu_dirty) {
graph_force_layout_gpu_upload(s_gpu_ctx, s_graph);
s_gpu_dirty = false;
}
s_state.layout_energy = graph_force_layout_gpu_step(s_gpu_ctx, cfg);
graph_force_layout_gpu_readback(s_gpu_ctx, s_graph, /*include_velocities=*/true);
} else {
// GPU no disponible: caer a CPU silenciosamente.
s_use_gpu = false;
s_state.layout_energy = graph_force_layout_step(s_graph, cfg);
}
} else {
s_state.layout_energy = graph_force_layout_step(s_graph, cfg);
}
const float per_node = s_graph.node_count > 0
? s_state.layout_energy / (float)s_graph.node_count
: 0.0f;
if (per_node < k_pause_per_node) ++s_low_energy_frames;
else s_low_energy_frames = 0;
if (graph_force_layout_should_pause(s_low_energy_frames,
k_pause_after_frames)) {
s_state.layout_running = false;
s_low_energy_frames = 0;
}
} else {
s_low_energy_frames = 0;
}
// Callbacks (issue 0049i): right-click abre popup contextual,
// double-click loguea el indice. Los callbacks corren dentro del
// frame ImGui — el caller puede usar OpenPopup directamente.
static int s_ctx_node = -1;
static bool s_ctx_open = false;
struct Cb {
static void on_ctx(int idx, ImVec2 /*pos*/, void* user) {
int* slot = (int*)user;
*slot = idx;
ImGui::OpenPopup("##graph_node_ctx");
}
static void on_dbl(int idx, void* /*user*/) {
std::printf("[graph] dbl-click on node %d\n", idx);
}
};
GraphViewportCallbacks cb;
cb.on_context_menu = &Cb::on_ctx;
cb.on_double_click = &Cb::on_dbl;
cb.user = &s_ctx_node;
graph_viewport("##graph_demo", s_graph, s_state, ImVec2(0, 460), cb);
// Labels overlay (issue 0049j). El callback formatea "#<idx>" en un
// buffer estatico por demo — apps reales (osint_graph) usaran un
// string pool de la BD origen.
if (s_labels_enabled) {
struct LblCtx { char buf[32]; };
static LblCtx s_lbl_ctx;
auto get_label = [](int idx, void* user) -> const char* {
auto* ctx = static_cast<LblCtx*>(user);
std::snprintf(ctx->buf, sizeof(ctx->buf), "#%d", idx);
return ctx->buf;
};
graph::graph_labels_draw(s_graph, s_state, s_label_policy,
get_label, &s_lbl_ctx);
}
if (ImGui::BeginPopup("##graph_node_ctx")) {
ImGui::Text("Node #%d", s_ctx_node);
ImGui::Separator();
if (ImGui::MenuItem("Pin")) {
if (s_ctx_node >= 0 && s_ctx_node < s_graph.node_count)
s_graph.nodes[s_ctx_node].flags |= NF_PINNED;
}
if (ImGui::MenuItem("Unpin")) {
if (s_ctx_node >= 0 && s_ctx_node < s_graph.node_count)
s_graph.nodes[s_ctx_node].flags &= ~NF_PINNED;
}
if (ImGui::MenuItem("Add to selection")) {
graph_viewport_add_to_selection(s_graph, s_state, s_ctx_node);
}
ImGui::EndPopup();
}
// Overlay con count seleccionados (lasso/multi-select feedback).
if (!s_state.selection.empty()) {
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text);
ImGui::Text("[%zu selected]", s_state.selection.size());
ImGui::PopStyleColor();
}
}
code_block(
"static GraphData graph;\n"
"static GraphViewportState state;\n"
"// ... rellenar graph.nodes / graph.edges ...\n"
"graph.update_bounds();\n"
"\n"
"// Por frame:\n"
"if (state.layout_running) {\n"
" ForceLayoutConfig cfg;\n"
" cfg.repulsion = 3500; cfg.gravity = 0.001f;\n"
" graph_force_layout_step(graph, cfg);\n"
"}\n"
"graph_viewport(\"##g\", graph, state, ImVec2(0, 460));"
);
}
} // namespace gallery
+243
View File
@@ -0,0 +1,243 @@
#include "demos.h"
#include "demo.h"
#include "viz/graph_types.h"
#include "viz/graph_viewport.h"
#include "viz/graph_renderer.h"
#include "viz/graph_force_layout.h"
#include "viz/graph_icons.h"
#include "core/button.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cmath>
#include <cstdio>
#include <vector>
namespace gallery {
// 6 codepoints Tabler representativos para los 6 EntityTypes del demo. El
// orden coincide con `s_entity_types[i]`: cada tipo apunta a `icon_id = i+1`
// (las regiones del atlas son 1-indexed; 0 reservado para "sin icono").
static const uint16_t k_demo_codepoints[6] = {
0xEB4Du, // TI_USER
0xEAE5u, // TI_MAIL
0xEAB9u, // TI_GLOBE
0xEB09u, // TI_PHONE
0xEA4Fu, // TI_BUILDING
0xEA88u, // TI_DATABASE
};
static const uint32_t k_styles_palette[6] = {
0xFF6BCB77u, // verde — Person (circle)
0xFFFF6B6Bu, // rojo — Email (square)
0xFF4D96FFu, // azul — Domain (diamond)
0xFFFFC75Fu, // ambar — Phone (hex)
0xFFC780E8u, // morado — Org (triangle)
0xFF52CDF2u, // cyan — Database (rounded square)
};
static const char* k_styles_names[6] = {
"Person", "Email", "Domain", "Phone", "Organization", "Database"
};
static EntityType s_entity_types[6];
static RelationType s_relation_types[3]; // solid, dashed, dotted
static IconAtlas* s_atlas = nullptr;
static bool s_types_initialized = false;
static bool s_atlas_bound = false;
static void init_demo_types() {
if (s_types_initialized) return;
for (int i = 0; i < 6; ++i) {
EntityType t{};
t.color = k_styles_palette[i];
t.shape = (uint8_t)(SHAPE_CIRCLE + i); // 1..6 — uno por shape
t.icon_id = (uint16_t)(i + 1); // 1-based
t.default_size = 14.0f;
t.name = k_styles_names[i];
s_entity_types[i] = t;
}
s_relation_types[0] = relation_type(0xFFCCCCCCu, EDGE_SOLID, 1.5f, "knows");
s_relation_types[1] = relation_type(0xFFFFB870u, EDGE_DASHED, 1.5f, "uses");
s_relation_types[2] = relation_type(0xFF89E0FCu, EDGE_DOTTED, 1.5f, "owns");
s_types_initialized = true;
}
// 30 nodos posicionados en un anillo por tipo. Aristas: cada nodo conecta a
// sus dos vecinos (arc) y a un nodo "central" del cluster siguiente. Mezcla
// de directed/undirected para validar las flechas.
static void build_demo_graph(std::vector<GraphNode>& nodes,
std::vector<GraphEdge>& edges)
{
nodes.clear();
edges.clear();
const int per_type = 5;
const float ring_r = 80.0f;
const float type_r = 30.0f;
for (int t = 0; t < 6; ++t) {
float ang_t = (float)t * (2.0f * 3.14159265f / 6.0f);
float cx = std::cos(ang_t) * ring_r;
float cy = std::sin(ang_t) * ring_r;
for (int k = 0; k < per_type; ++k) {
float a = (float)k * (2.0f * 3.14159265f / per_type) + ang_t * 0.3f;
GraphNode n = graph_node(cx + std::cos(a) * type_r,
cy + std::sin(a) * type_r,
(uint16_t)t);
n.user_data = (uint64_t)nodes.size();
nodes.push_back(n);
}
}
auto idx = [&](int t, int k) { return (uint32_t)(t * per_type + k); };
for (int t = 0; t < 6; ++t) {
// Aristas intra-cluster (knows = solid, undirected).
for (int k = 0; k < per_type; ++k) {
int next_k = (k + 1) % per_type;
GraphEdge e = graph_edge(idx(t, k), idx(t, next_k), 1.0f, /*type_id=*/0);
edges.push_back(e);
}
// Inter-cluster: del nodo 0 del cluster t al nodo 0 del cluster t+1
// como "uses" (dashed, directed).
int t_next = (t + 1) % 6;
GraphEdge e1 = graph_edge(idx(t, 0), idx(t_next, 0), 1.0f, /*type_id=*/1);
e1.flags |= EF_DIRECTED;
edges.push_back(e1);
// Y otra inter-cluster mas larga al cluster +2 como "owns" (dotted,
// directed). Asi se ven las 3 estilos a la vez.
int t_far = (t + 2) % 6;
GraphEdge e2 = graph_edge(idx(t, 2), idx(t_far, 3), 0.6f, /*type_id=*/2);
e2.flags |= EF_DIRECTED;
edges.push_back(e2);
}
}
void demo_graph_styles() {
demo_header("graph_renderer (shapes + icons + arrows + edge styles)", "v1.5.0",
"OSINT-style: 6 EntityTypes, uno por shape (circle, square, diamond, hex, "
"triangle, rounded square) con icono Tabler en el centro. 3 RelationTypes "
"(solid/dashed/dotted) con flechas en los aristas EF_DIRECTED. Mismas dos "
"draw calls que el viewport normal (1 nodos + 1 aristas).");
init_demo_types();
static std::vector<GraphNode> s_nodes;
static std::vector<GraphEdge> s_edges;
static GraphData s_graph{};
static GraphViewportState s_state;
static bool s_initialized = false;
static bool s_run_layout = false;
if (!s_initialized) {
build_demo_graph(s_nodes, s_edges);
s_graph.nodes = s_nodes.data();
s_graph.node_count = (int)s_nodes.size();
s_graph.node_capacity = (int)s_nodes.capacity();
s_graph.edges = s_edges.data();
s_graph.edge_count = (int)s_edges.size();
s_graph.edge_capacity = (int)s_edges.capacity();
s_graph.types = s_entity_types;
s_graph.type_count = 6;
s_graph.rel_types = s_relation_types;
s_graph.rel_type_count = 3;
s_graph.update_bounds();
s_state.layout_running = false; // queremos ver las shapes posicionadas, no el caos del force
s_state.zoom = 2.0f;
s_initialized = true;
}
section("Legend");
{
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
for (int i = 0; i < 6; ++i) {
ImGui::Text("%-13s shape=%d icon_id=%d color=#%06x",
k_styles_names[i],
(int)s_entity_types[i].shape,
(int)s_entity_types[i].icon_id,
(unsigned)(s_entity_types[i].color & 0x00FFFFFFu));
}
ImGui::Text("Edges: knows=solid, uses=dashed (directed), owns=dotted (directed)");
ImGui::PopStyleColor();
}
section("Controls");
{
using namespace fn_ui;
if (button(s_run_layout ? "Pause force layout" : "Run force layout",
ButtonVariant::Secondary)) {
s_run_layout = !s_run_layout;
s_state.layout_running = s_run_layout;
}
ImGui::SameLine();
if (button("Rebuild", ButtonVariant::Subtle)) {
build_demo_graph(s_nodes, s_edges);
s_graph.nodes = s_nodes.data();
s_graph.node_count = (int)s_nodes.size();
s_graph.edges = s_edges.data();
s_graph.edge_count = (int)s_edges.size();
s_graph.update_bounds();
}
ImGui::SameLine();
if (button("Fit view", ButtonVariant::Subtle)) {
graph_viewport_fit(s_graph, s_state);
}
}
section("Viewport");
if (s_run_layout) {
ForceLayoutConfig cfg;
cfg.repulsion = 1500.0f;
cfg.attraction = 0.04f;
cfg.gravity = 0.005f;
cfg.iterations = 1;
graph_force_layout_step(s_graph, cfg);
}
// El viewport crea internamente el GraphRenderer. La primera vez que se
// dibuja el panel, el renderer existe — bindeamos el atlas justo despues.
graph_viewport("##graph_styles", s_graph, s_state, ImVec2(0, 460));
if (!s_atlas_bound && s_state.renderer) {
s_atlas = graph_icons_build(k_demo_codepoints, 6, 32);
if (s_atlas) {
graph_renderer_set_icon_atlas(s_state.renderer,
graph_icons_texture(s_atlas),
graph_icons_uv_table(s_atlas),
graph_icons_count(s_atlas));
s_atlas_bound = true;
} else {
// Sin atlas: marcamos como bound para no reintentar cada frame —
// el renderer simplemente pinta sin overlay de iconos.
s_atlas_bound = true;
}
}
code_block(
"// Build atlas con 6 codepoints Tabler\n"
"const uint16_t cps[] = {0xEB4D, 0xEAE5, 0xEAB9, 0xEB09, 0xEA4F, 0xEA88};\n"
"IconAtlas* atlas = graph_icons_build(cps, 6, 32);\n"
"\n"
"// EntityTypes: cada uno con su shape e icono\n"
"EntityType person = {0xFF6BCB77, SHAPE_CIRCLE, /*icon_id=*/1, 14, \"Person\"};\n"
"EntityType email = {0xFFFF6B6B, SHAPE_SQUARE, /*icon_id=*/2, 14, \"Email\"};\n"
"// ... etc\n"
"\n"
"// RelationTypes: solid / dashed / dotted\n"
"RelationType knows = relation_type(0xFFCCCCCC, EDGE_SOLID, 1.5f, \"knows\");\n"
"RelationType uses = relation_type(0xFFFFB870, EDGE_DASHED, 1.5f, \"uses\");\n"
"\n"
"// Bind atlas al renderer\n"
"graph_renderer_set_icon_atlas(renderer, graph_icons_texture(atlas),\n"
" graph_icons_uv_table(atlas),\n"
" graph_icons_count(atlas));\n"
"\n"
"// Aristas direccionales\n"
"GraphEdge e = graph_edge(src, tgt, 1.0f, /*type_id=*/1);\n"
"e.flags |= EF_DIRECTED;");
}
} // namespace gallery
+108
View File
@@ -0,0 +1,108 @@
// Demo del primitivo viz/mesh_viewer.
// Genera un cubo procedural in-line, lo sube al GPU, y permite cargar un
// .obj desde un path ingresado en un text input.
#include "demos.h"
#include "demo.h"
#include "viz/mesh_viewer.h"
#include "gfx/mesh_obj_load.h"
#include "gfx/mesh_gpu.h"
#include "core/orbit_camera.h"
#include <imgui.h>
#include <cstring>
#include <string>
namespace gallery {
namespace {
const char* kCubeObj =
"v -1 -1 -1\nv 1 -1 -1\nv 1 1 -1\nv -1 1 -1\n"
"v -1 -1 1\nv 1 -1 1\nv 1 1 1\nv -1 1 1\n"
"f 4 3 2 1\n" // back (-Z) — winding for outward normal
"f 5 6 7 8\n" // front (+Z)
"f 1 2 6 5\n" // bottom (-Y)
"f 8 7 3 4\n" // top (+Y)
"f 5 8 4 1\n" // left (-X)
"f 2 3 7 6\n"; // right (+X)
struct State {
fn::gfx::MeshGpu mesh{};
fn::core::OrbitCamera cam{};
char path[512] = "";
std::string status;
bool wireframe = false;
bool initialized = false;
};
State& state() {
static State s;
return s;
}
void load_cube() {
auto& s = state();
if (s.mesh.ok()) fn::gfx::mesh_gpu_destroy(s.mesh);
auto cpu = fn::gfx::mesh_obj_parse(kCubeObj, std::strlen(kCubeObj));
s.mesh = fn::gfx::mesh_gpu_upload(cpu);
s.status = s.mesh.ok()
? ("loaded cube: " + std::to_string(s.mesh.index_count / 3) + " tris")
: "cube upload failed";
}
void load_from_path() {
auto& s = state();
if (!s.path[0]) { s.status = "path is empty"; return; }
auto cpu = fn::gfx::mesh_obj_load(s.path);
if (cpu.positions.empty()) { s.status = "parse/read failed"; return; }
if (s.mesh.ok()) fn::gfx::mesh_gpu_destroy(s.mesh);
s.mesh = fn::gfx::mesh_gpu_upload(cpu);
s.status = s.mesh.ok()
? ("loaded: " + std::to_string(s.mesh.index_count / 3) + " tris")
: "upload failed";
}
} // namespace
void demo_mesh_viewer() {
demo_header("mesh_viewer", "v1.0.0",
"Visualizador 3D para inspeccion de geometria. Composicion de "
"mesh_obj_load (parser .obj puro) + mesh_gpu (upload VAO/VBO/EBO) + "
"orbit_camera (drag/wheel) + mesh_viewer (FBO + ImGui::Image + Lambert).");
auto& s = state();
if (!s.initialized) {
load_cube();
s.initialized = true;
}
// Controls row.
if (ImGui::Button("Reload cube")) load_cube();
ImGui::SameLine();
ImGui::Checkbox("Wireframe", &s.wireframe);
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
ImGui::SetNextItemWidth(360);
ImGui::InputTextWithHint("##obj_path", "absolute path to .obj", s.path, sizeof(s.path));
ImGui::SameLine();
if (ImGui::Button("Load .obj")) load_from_path();
ImGui::TextDisabled("status: %s | tris: %d | drag to orbit, wheel to zoom",
s.status.c_str(),
s.mesh.ok() ? s.mesh.index_count / 3 : 0);
ImGui::Separator();
fn::viz::MeshViewerConfig cfg{};
cfg.mesh = &s.mesh;
cfg.cam = &s.cam;
cfg.size = ImVec2(-1.0f, 480.0f);
cfg.color = IM_COL32(160, 200, 255, 255);
cfg.wireframe = s.wireframe;
fn::viz::mesh_viewer("##gallery_mesh_viewer", cfg);
}
} // namespace gallery
+208
View File
@@ -0,0 +1,208 @@
// demos_scientific.cpp — demos para los 5 charts cientificos del issue 0034:
// treemap, sankey, chord, contour, voronoi.
#include "demos.h"
#include "demo.h"
#include "viz/treemap.h"
#include "viz/sankey.h"
#include "viz/chord.h"
#include "viz/contour.h"
#include "viz/voronoi.h"
#include <imgui.h>
#include <cmath>
#include <cstdlib>
#include <vector>
namespace gallery {
// ---------------------------------------------------------------------------
// treemap
// ---------------------------------------------------------------------------
void demo_treemap() {
demo_header("treemap", "v1.0.0",
"Squarified treemap (Bruls et al.) para jerarquias planas con valores. "
"Algoritmo puro separado del render.");
section("Gastos por categoria");
{
std::vector<TreemapItem> items = {
{"vivienda", 950.0f, IM_COL32(180, 120, 200, 255)},
{"comida", 320.0f, IM_COL32(120, 180, 200, 255)},
{"transporte", 180.0f, IM_COL32(200, 180, 120, 255)},
{"ocio", 140.0f, IM_COL32(200, 120, 160, 255)},
{"salud", 90.0f, IM_COL32(120, 200, 160, 255)},
{"otros", 60.0f, IM_COL32(160, 160, 200, 255)},
};
treemap("##gastos", items, ImVec2(-1, 320));
}
code_block(
"std::vector<TreemapItem> items = {\n"
" {\"vivienda\", 950.0f, IM_COL32(180,120,200,255)},\n"
" {\"comida\", 320.0f, IM_COL32(120,180,200,255)},\n"
" ...\n"
"};\n"
"treemap(\"##gastos\", items, ImVec2(-1, 320));"
);
}
// ---------------------------------------------------------------------------
// sankey
// ---------------------------------------------------------------------------
void demo_sankey() {
demo_header("sankey", "v1.0.0",
"Sankey diagram para flujos source -> target. BFS topologico para columnas, "
"bandas curvas (bezier cubico) para los links. Asume DAG.");
section("Clientes -> productos -> categorias");
{
std::vector<SankeyNode> nodes = {
{"premium"}, {"basicos"},
{"laptops"}, {"phones"}, {"tablets"},
{"hardware"}, {"software"}, {"servicios"},
};
std::vector<SankeyLink> links = {
// clientes -> productos
{0, 2, 80}, {0, 3, 30}, {0, 4, 15},
{1, 3, 60}, {1, 4, 40}, {1, 2, 20},
// productos -> categorias
{2, 5, 70}, {2, 6, 30},
{3, 5, 50}, {3, 7, 40},
{4, 6, 35}, {4, 7, 20},
};
sankey("##flow", nodes, links, ImVec2(-1, 360));
}
code_block(
"std::vector<SankeyNode> nodes = {{\"premium\"}, {\"basicos\"}, ...};\n"
"std::vector<SankeyLink> links = {{0, 2, 80}, {0, 3, 30}, ...};\n"
"sankey(\"##flow\", nodes, links, ImVec2(-1, 360));"
);
}
// ---------------------------------------------------------------------------
// chord
// ---------------------------------------------------------------------------
void demo_chord() {
demo_header("chord", "v1.0.0",
"Chord diagram para matrices NxN. Arcos proporcionales a sum(row) + cuerdas "
"internas con bezier cubico.");
section("Flujos entre paises (matriz 6x6 simetrica)");
{
constexpr int N = 6;
// simetrica de "comercio" entre 6 paises
static float M[N * N] = {
0, 10, 6, 12, 4, 3,
10, 0, 14, 3, 8, 2,
6, 14, 0, 9, 11, 5,
12, 3, 9, 0, 7, 6,
4, 8, 11, 7, 0, 13,
3, 2, 5, 6, 13, 0,
};
static const char* labels[N] = {"ESP", "FRA", "ITA", "DEU", "PRT", "GBR"};
chord("##chord", M, N, labels, ImVec2(420, 420));
}
code_block(
"float M[N*N] = { // simetrica\n"
" 0, 10, 6, 12, 4, 3,\n"
" 10, 0, 14, 3, 8, 2,\n"
" ...\n"
"};\n"
"const char* labels[6] = {\"ESP\",\"FRA\",\"ITA\",\"DEU\",\"PRT\",\"GBR\"};\n"
"chord(\"##c\", M, 6, labels);"
);
}
// ---------------------------------------------------------------------------
// contour
// ---------------------------------------------------------------------------
void demo_contour() {
demo_header("contour", "v1.0.0",
"Contour plot 2D via marching squares. Para una gaussiana centrada los "
"contornos resultantes son aproximadamente concentricos.");
constexpr int N = 32;
static float grid[N * N];
static bool init = false;
if (!init) {
// Mezcla de 2 gaussianas (peak central + secundario)
for (int y = 0; y < N; y++) {
for (int x = 0; x < N; x++) {
float dx1 = x - N * 0.45f, dy1 = y - N * 0.5f;
float dx2 = x - N * 0.75f, dy2 = y - N * 0.3f;
float v = std::exp(-(dx1 * dx1 + dy1 * dy1) / 70.0f)
+ 0.55f * std::exp(-(dx2 * dx2 + dy2 * dy2) / 30.0f);
grid[y * N + x] = v;
}
}
init = true;
}
static const float levels[] = {0.15f, 0.30f, 0.50f, 0.70f, 0.90f};
contour("##gauss", grid, N, N, levels, 5, ImVec2(-1, 320));
code_block(
"constexpr int N = 32;\n"
"float grid[N*N];\n"
"for (int y = 0; y < N; y++)\n"
" for (int x = 0; x < N; x++) {\n"
" float dx = x - N/2.0f, dy = y - N/2.0f;\n"
" grid[y*N + x] = std::exp(-(dx*dx + dy*dy) / 80.0f);\n"
" }\n"
"float levels[] = {0.15f, 0.30f, 0.50f, 0.70f, 0.90f};\n"
"contour(\"##gauss\", grid, N, N, levels, 5);"
);
}
// ---------------------------------------------------------------------------
// voronoi
// ---------------------------------------------------------------------------
void demo_voronoi() {
demo_header("voronoi", "v1.0.0",
"Diagrama de Voronoi via raster brute-force (MVP). Tiles 4x4 px coloreados "
"por el seed mas cercano. Suficiente para N <= 200.");
constexpr int N = 30;
static ImVec2 seeds [N];
static ImU32 colors[N];
static bool init = false;
if (!init) {
unsigned seed = 7;
auto rnd = [&]() {
seed = seed * 1103515245u + 12345u;
return (float)((seed >> 16) & 0x7fff) / 32768.0f;
};
// El render escala automaticamente; las posiciones se asumen en coords del rect.
// Como no sabemos W/H aqui, usamos coords aproximadas para 600x300 y el clip
// dentro de voronoi se encarga de mantenerlas en rango.
for (int i = 0; i < N; i++) {
seeds [i] = ImVec2(rnd() * 600.0f, rnd() * 300.0f);
colors[i] = IM_COL32(40 + (int)(rnd() * 200),
40 + (int)(rnd() * 200),
60 + (int)(rnd() * 195),
230);
}
init = true;
}
voronoi("##v", seeds, N, colors, ImVec2(-1, 300));
code_block(
"ImVec2 seeds[30];\n"
"ImU32 colors[30];\n"
"for (int i = 0; i < 30; i++) {\n"
" seeds [i] = ImVec2(rnd() * 600.0f, rnd() * 300.0f);\n"
" colors[i] = IM_COL32(rnd_byte(), rnd_byte(), rnd_byte(), 230);\n"
"}\n"
"voronoi(\"##v\", seeds, 30, colors, ImVec2(-1, 300));"
);
}
} // namespace gallery
+129
View File
@@ -0,0 +1,129 @@
// Demo de sql_workbench (Core, issue 0032).
//
// Abre `registry.db` en modo readonly y deja que el componente liste sus
// tablas en la sidebar. La idea es probar el ciclo Run + tabla + historial
// contra una DB real sin riesgo de mutarla.
#include "demos.h"
#include "demo.h"
#include "core/sql_workbench.h"
#include "core/tokens.h"
#include <imgui.h>
#include <sqlite3.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
namespace gallery {
namespace {
struct SqlDemoState {
sqlite3* db = nullptr;
fn::SqlWorkbenchState wb;
bool tried_open = false;
std::string db_path;
std::string open_error;
};
SqlDemoState& state() {
static SqlDemoState s;
return s;
}
// Resuelve la ruta a registry.db: env FN_REGISTRY_ROOT/registry.db si existe,
// si no, prueba ./registry.db, ../registry.db, ../../registry.db (build tree).
std::string resolve_registry_db() {
if (const char* env = std::getenv("FN_REGISTRY_ROOT")) {
std::string p = std::string(env) + "/registry.db";
if (FILE* f = std::fopen(p.c_str(), "rb")) { std::fclose(f); return p; }
}
const char* candidates[] = {
"registry.db",
"../registry.db",
"../../registry.db",
"../../../registry.db",
"../../../../registry.db",
};
for (const char* c : candidates) {
if (FILE* f = std::fopen(c, "rb")) { std::fclose(f); return c; }
}
return "";
}
void ensure_open() {
auto& s = state();
if (s.tried_open) return;
s.tried_open = true;
s.db_path = resolve_registry_db();
if (s.db_path.empty()) {
s.open_error = "registry.db not found (tried FN_REGISTRY_ROOT and parent dirs)";
return;
}
int rc = sqlite3_open_v2(s.db_path.c_str(), &s.db,
SQLITE_OPEN_READONLY, nullptr);
if (rc != SQLITE_OK) {
s.open_error = sqlite3_errmsg(s.db);
if (s.db) { sqlite3_close(s.db); s.db = nullptr; }
return;
}
s.wb.readonly = true;
// Query inicial mas util para el demo: lista de funciones del registry.
s.wb.query =
"SELECT id, kind, purity, domain\n"
"FROM functions\n"
"ORDER BY id\n"
"LIMIT 50;";
}
} // namespace
void demo_sql_workbench() {
using namespace fn_tokens;
demo_header("sql_workbench", "v1.0.0",
"Workbench SQL: editor con highlighting, schema sidebar, tabla de "
"resultados e historial. Ejecuta queries contra una sqlite3* del caller. "
"En este demo, registry.db abierto en modo readonly.");
ensure_open();
auto& s = state();
if (!s.open_error.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::error);
ImGui::TextWrapped("could not open registry.db: %s", s.open_error.c_str());
ImGui::PopStyleColor();
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::TextWrapped("Set FN_REGISTRY_ROOT to the repo root or run from the repo cwd.");
ImGui::PopStyleColor();
return;
}
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::Text("db: %s (readonly)", s.db_path.c_str());
ImGui::PopStyleColor();
section("workbench");
{
ImVec2 avail = ImGui::GetContentRegionAvail();
// Reserva un pelin para el code_block de abajo.
float h = avail.y - 110.0f;
if (h < 320.0f) h = 320.0f;
fn::sql_workbench("##gallery_sql", s.db, s.wb, ImVec2(-1, h));
}
code_block(
"sqlite3* db = nullptr;\n"
"sqlite3_open_v2(\"registry.db\", &db, SQLITE_OPEN_READONLY, nullptr);\n"
"fn::SqlWorkbenchState st;\n"
"st.readonly = true;\n"
"fn::sql_workbench(\"##sql\", db, st, ImVec2(-1, -1));"
);
}
} // namespace gallery
+279
View File
@@ -0,0 +1,279 @@
// Demos individuales de text_editor y file_watcher (Wave 1, issue 0025).
//
// Aunque las dos primitivas estan diseñadas para componerse, en gallery se
// muestran por separado para que cada entry exhiba un solo primitivo y su
// API minima.
#include "demos.h"
#include "demo.h"
#include "core/text_editor.h"
#include "core/file_watcher.h"
#include "core/button.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <deque>
#include <string>
namespace gallery {
// ===========================================================================
// text_editor — editor de codigo con syntax highlighting
// ===========================================================================
namespace {
const char* kSampleGLSL =
"#version 330\n"
"// fragment shader demo\n"
"out vec4 frag_color;\n"
"uniform vec2 u_resolution;\n"
"uniform float u_time;\n"
"\n"
"void main() {\n"
" vec2 uv = gl_FragCoord.xy / u_resolution;\n"
" vec3 col = 0.5 + 0.5 * cos(u_time + uv.xyx + vec3(0,2,4));\n"
" frag_color = vec4(col, 1.0);\n"
"}\n";
const char* kSampleSQL =
"-- fts5 search sobre el registry\n"
"SELECT id, kind, purity, description\n"
"FROM functions\n"
"WHERE id IN (\n"
" SELECT id FROM functions_fts\n"
" WHERE functions_fts MATCH 'name:slic* OR description:slic*'\n"
")\n"
"ORDER BY name\n"
"LIMIT 50;\n";
const char* kSampleCpp =
"#include <imgui.h>\n"
"namespace fn {\n"
" bool button(const char* label, ButtonVariant v) {\n"
" auto& tk = tokens::current();\n"
" ImGui::PushStyleColor(ImGuiCol_Button, tk.bg_for(v));\n"
" bool clicked = ImGui::Button(label);\n"
" ImGui::PopStyleColor();\n"
" return clicked;\n"
" }\n"
"}\n";
struct EditorState {
fn::TextEditorState* ed = nullptr;
fn::CodeLang lang = fn::CodeLang::GLSL;
};
EditorState& editor_state() {
static EditorState s;
return s;
}
void ensure_editor() {
auto& s = editor_state();
if (!s.ed) {
s.ed = fn::text_editor_create(s.lang);
fn::text_editor_set_text(s.ed, kSampleGLSL);
}
}
void apply_language(fn::CodeLang next) {
auto& s = editor_state();
if (next == s.lang) return;
fn::text_editor_destroy(s.ed);
s.ed = fn::text_editor_create(next);
s.lang = next;
switch (next) {
case fn::CodeLang::GLSL: fn::text_editor_set_text(s.ed, kSampleGLSL); break;
case fn::CodeLang::SQL: fn::text_editor_set_text(s.ed, kSampleSQL); break;
case fn::CodeLang::Cpp: fn::text_editor_set_text(s.ed, kSampleCpp); break;
case fn::CodeLang::Generic: fn::text_editor_set_text(s.ed, ""); break;
}
}
} // namespace
void demo_text_editor() {
using namespace fn_tokens;
demo_header("text_editor", "v1.0.0",
"Editor de codigo embebido en ImGui con syntax highlighting (GLSL/SQL/Cpp/Generic). "
"Wrapper PIMPL sobre ImGuiColorTextEdit (MIT). API: create / set_text / get_text / "
"render / is_dirty.");
ensure_editor();
auto& s = editor_state();
section("language");
{
const char* labels[] = {"GLSL", "SQL", "Cpp", "Generic"};
const fn::CodeLang langs[] = {
fn::CodeLang::GLSL, fn::CodeLang::SQL, fn::CodeLang::Cpp, fn::CodeLang::Generic
};
for (int i = 0; i < 4; ++i) {
if (i > 0) ImGui::SameLine();
bool active = (s.lang == langs[i]);
if (active) ImGui::PushStyleColor(ImGuiCol_Button, colors::primary);
if (ImGui::Button(labels[i])) apply_language(langs[i]);
if (active) ImGui::PopStyleColor();
}
}
section("editor");
{
ImVec2 avail = ImGui::GetContentRegionAvail();
float h = avail.y - 60.0f;
if (h < 220.0f) h = 220.0f;
fn::text_editor_render(s.ed, "##fn_text_editor_solo", ImVec2(-1, h));
if (fn::text_editor_is_dirty(s.ed)) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::warning);
ImGui::TextUnformatted("(modified)");
ImGui::PopStyleColor();
ImGui::SameLine();
if (ImGui::Button("clear dirty##te_solo")) fn::text_editor_clear_dirty(s.ed);
} else {
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::TextUnformatted("(clean)");
ImGui::PopStyleColor();
}
}
code_block(
"auto* ed = fn::text_editor_create(fn::CodeLang::GLSL);\n"
"fn::text_editor_set_text(ed, src);\n"
"if (fn::text_editor_render(ed, \"##ed\", {600, 400}))\n"
" on_changed(fn::text_editor_get_text(ed));"
);
}
// ===========================================================================
// file_watcher — watcher cross-platform no bloqueante
// ===========================================================================
namespace {
constexpr const char* kWatchPath = "/tmp/fn_demo.glsl";
struct WatcherDemoState {
fn::FileWatcher* fw = nullptr;
bool active = false;
std::string err;
std::deque<std::string> events;
};
WatcherDemoState& watcher_state() {
static WatcherDemoState s;
return s;
}
void ensure_watcher() {
auto& s = watcher_state();
if (!s.fw) {
s.fw = fn::file_watcher_create();
s.active = fn::file_watcher_add(s.fw, kWatchPath);
if (!s.active) s.err = fn::file_watcher_last_error(s.fw);
}
}
const char* kind_label(fn::FileEvent::Kind k) {
switch (k) {
case fn::FileEvent::Modified: return "MODIFIED";
case fn::FileEvent::Created: return "CREATED";
case fn::FileEvent::Deleted: return "DELETED";
}
return "?";
}
void poll_and_log() {
auto& s = watcher_state();
if (!s.fw) return;
auto evs = fn::file_watcher_poll(s.fw);
for (auto& e : evs) {
char buf[512];
std::snprintf(buf, sizeof(buf), "[%s] %s", kind_label(e.kind), e.path.c_str());
s.events.push_back(buf);
}
while (s.events.size() > 200) s.events.pop_front();
}
bool touch_demo_file(std::string& err_out) {
FILE* f = std::fopen(kWatchPath, "a");
if (!f) { err_out = std::strerror(errno); return false; }
std::fprintf(f, "// touch %ld\n", (long)std::time(nullptr));
std::fclose(f);
return true;
}
} // namespace
void demo_file_watcher() {
using namespace fn_tokens;
demo_header("file_watcher", "v1.0.0",
"Watcher de archivos cross-platform no bloqueante. Linux: inotify. Windows: "
"ReadDirectoryChangesW. API: create / add / poll (drain) / destroy. Cap del "
"buffer de eventos: 200.");
ensure_watcher();
poll_and_log();
auto& s = watcher_state();
section("watcher state");
ImGui::Text("path: %s", kWatchPath);
ImGui::Text("active: %s", s.active ? "yes" : "no");
if (!s.err.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::error);
ImGui::TextWrapped("err: %s", s.err.c_str());
ImGui::PopStyleColor();
}
section("trigger events");
{
if (ImGui::Button("touch (append timestamp)")) {
std::string e;
if (!touch_demo_file(e)) s.err = "touch failed: " + e;
else s.err.clear();
// Si el archivo no existia al inicio, reintenta el add.
if (!s.active) {
s.active = fn::file_watcher_add(s.fw, kWatchPath);
if (!s.active) s.err = fn::file_watcher_last_error(s.fw);
}
}
ImGui::SameLine();
if (ImGui::Button("clear events")) s.events.clear();
ImGui::SameLine();
ImGui::TextDisabled("(o desde otro terminal: echo hi >> %s)", kWatchPath);
}
section("event log");
ImGui::Text("captured: %d", (int)s.events.size());
ImGui::BeginChild("##fw_evlog", ImVec2(0, 0), ImGuiChildFlags_Borders);
if (s.events.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::TextWrapped("Sin eventos. Pulsa touch o modifica el path desde otro terminal.");
ImGui::PopStyleColor();
} else {
for (auto it = s.events.rbegin(); it != s.events.rend(); ++it) {
ImGui::TextUnformatted(it->c_str());
}
}
ImGui::EndChild();
code_block(
"auto* fw = fn::file_watcher_create();\n"
"fn::file_watcher_add(fw, \"/tmp/foo.glsl\");\n"
"for (auto& e : fn::file_watcher_poll(fw)) {\n"
" handle_event(e.path, e.kind);\n"
"}"
);
}
} // namespace gallery
+211
View File
@@ -0,0 +1,211 @@
#include "demos.h"
#include "demo.h"
#include "viz/bar_chart.h"
#include "viz/pie_chart.h"
#include "viz/line_plot.h"
#include "viz/scatter_plot.h"
#include "viz/histogram.h"
#include "viz/sparkline.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cmath>
#include <vector>
namespace gallery {
// ---------------------------------------------------------------------------
// bar_chart
// ---------------------------------------------------------------------------
void demo_bar_chart() {
demo_header("bar_chart", "v1.2.0",
"Barras verticales con ejes pineados, tooltip al hover y auto-rotacion 45 grados "
"de labels cuando no caben horizontalmente.");
section("Labels que caben horizontalmente");
{
const char* langs[] = {"go", "py", "ts", "sh", "cpp"};
float values[] = {412.0f, 187.0f, 94.0f, 63.0f, 36.0f};
bar_chart("##bar_short", langs, values, 5, 0.67f, 200.0f);
}
section("Labels largos que obligan a rotar");
{
const char* domains[] = {
"core", "infrastructure", "finance", "datascience",
"cybersecurity", "notebook", "browser"
};
float values[] = {412, 187, 94, 63, 42, 38, 22};
bar_chart("##bar_long", domains, values, 7, 0.67f, 240.0f);
}
code_block(
"const char* labels[] = {\"go\",\"py\",\"ts\",\"sh\",\"cpp\"};\n"
"float values[] = {412,187,94,63,36};\n"
"bar_chart(\"##lang\", labels, values, 5); // h=200 default\n"
"bar_chart(\"##lang\", labels, values, 5, 0.8f, 300); // bar_w + altura"
);
}
// ---------------------------------------------------------------------------
// pie_chart
// ---------------------------------------------------------------------------
void demo_pie_chart() {
demo_header("pie_chart", "v1.1.0",
"Pie/donut con aspect 1:1, ejes pineados y tooltip por slice con "
"valor absoluto + porcentaje.");
if (ImGui::BeginTable("##pie_grid", 2, ImGuiTableFlags_SizingStretchSame)) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
{
const char* labels[] = {"Pure", "Impure"};
float values[] = {412.0f, 278.0f};
variant_label("Pie (radius auto)");
pie_chart("##pie_auto", labels, values, 2, 0.0f, 260.0f);
}
ImGui::TableSetColumnIndex(1);
{
const char* labels[] = {"function", "pipeline", "component"};
float values[] = {618.0f, 42.0f, 230.0f};
variant_label("Donut (radius = -0.45)");
pie_chart("##pie_donut", labels, values, 3, -0.45f, 260.0f);
}
ImGui::EndTable();
}
code_block(
"const char* labels[] = {\"Pure\",\"Impure\"};\n"
"float values[] = {412, 278};\n"
"pie_chart(\"##p\", labels, values, 2); // pie auto\n"
"pie_chart(\"##p\", labels, values, 2, -0.45f, 260); // donut"
);
}
// ---------------------------------------------------------------------------
// line_plot
// ---------------------------------------------------------------------------
void demo_line_plot() {
demo_header("line_plot", "v1.1.0",
"Line plot 2D con limites de ejes calculados de min/max y pineados. "
"Sin auto-fit animado, sin pan/zoom.");
constexpr int N = 100;
static float xs[N], ys[N];
static bool init = false;
if (!init) {
for (int i = 0; i < N; i++) {
xs[i] = static_cast<float>(i) * 0.1f;
ys[i] = std::sin(xs[i]) + 0.3f * std::sin(xs[i] * 3.5f);
}
init = true;
}
line_plot("##line", xs, ys, N, 240.0f);
code_block(
"line_plot(\"##series\", xs, ys, count); // h=200 default\n"
"line_plot(\"##series\", xs, ys, count, 300.0f); // custom height"
);
}
// ---------------------------------------------------------------------------
// scatter_plot
// ---------------------------------------------------------------------------
void demo_scatter_plot() {
demo_header("scatter_plot", "v1.1.0",
"Puntos dispersos con ejes pineados (5% headroom). Sin interaccion.");
constexpr int N = 120;
static float xs[N], ys[N];
static bool init = false;
if (!init) {
unsigned seed = 1234;
auto rnd = [&]() {
seed = seed * 1103515245u + 12345u;
return static_cast<float>((seed >> 16) & 0x7fff) / 32768.0f;
};
for (int i = 0; i < N; i++) {
xs[i] = rnd() * 10.0f;
ys[i] = 0.5f * xs[i] + rnd() * 3.0f;
}
init = true;
}
scatter_plot("##sc", xs, ys, N, 240.0f);
code_block(
"scatter_plot(\"##xy\", xs, ys, count, 240.0f);"
);
}
// ---------------------------------------------------------------------------
// histogram
// ---------------------------------------------------------------------------
void demo_histogram() {
demo_header("histogram", "v1.1.0",
"Histograma con bins automaticos (Sturges) o manuales. Usa AutoFit "
"para los bins + Lock para bloquear pan/zoom.");
constexpr int N = 300;
static float vals[N];
static bool init = false;
if (!init) {
unsigned seed = 42;
auto rnd = [&]() {
seed = seed * 1103515245u + 12345u;
return static_cast<float>((seed >> 16) & 0x7fff) / 32768.0f;
};
// Aproximacion de distribucion normal via box-muller simplificado
for (int i = 0; i < N; i++) {
float u1 = rnd() + 1e-6f;
float u2 = rnd();
vals[i] = std::sqrt(-2.0f * std::log(u1))
* std::cos(2.0f * 3.14159f * u2);
}
init = true;
}
histogram("##hist", vals, N, -1, 240.0f);
code_block(
"histogram(\"##h\", values, count); // bins=Sturges\n"
"histogram(\"##h\", values, count, 30, 300.0f); // 30 bins, h=300"
);
}
// ---------------------------------------------------------------------------
// sparkline
// ---------------------------------------------------------------------------
void demo_sparkline() {
demo_header("sparkline", "v1.0.0",
"Mini grafico de lineas inline (rellenado con alpha + linea). "
"Pensado para tablas, KPI cards, headers.");
float up[] = {10, 12, 11, 15, 18, 17, 20};
float down[] = {30, 28, 29, 25, 22, 24, 20};
float flat[] = {10, 10, 10, 10, 10, 10, 10};
ImGui::Text("Trending up "); ImGui::SameLine();
sparkline("##up", up, 7, ImVec4(0.35f, 0.85f, 0.45f, 1.0f), 140.0f, 22.0f);
ImGui::Text("Trending down"); ImGui::SameLine();
sparkline("##down", down, 7, ImVec4(0.90f, 0.30f, 0.30f, 1.0f), 140.0f, 22.0f);
ImGui::Text("Flat "); ImGui::SameLine();
sparkline("##flat", flat, 7, ImVec4(0.55f, 0.55f, 0.55f, 1.0f), 140.0f, 22.0f);
code_block(
"float history[] = {10,12,11,15,18,17,20};\n"
"sparkline(\"##rev\", history, 7, /*color=*/{0.35,0.85,0.45,1}, 140, 22);"
);
}
} // namespace gallery
+230
View File
@@ -0,0 +1,230 @@
// primitives_gallery — catalogo visual interactivo de los primitivos UI
// del registry (cpp/functions/core y cpp/functions/viz).
//
// Sidebar izquierdo con lista de primitivos agrupados por dominio; panel
// derecho renderiza la demo del item seleccionado (+ snippet de codigo).
//
// Rol: smoke test visual + documentacion viva + build gate en CI.
// NO se conecta a sqlite_api ni a ningun backend. Datos sinteticos.
#include "app_base.h"
#include "imgui.h"
#include "core/fullscreen_window.h"
#include "core/tokens.h"
#include "core/page_header.h"
#include "core/toast.h"
#include "core/app_menubar.h"
#include "core/tree_view.h"
#include "demos.h"
#include "demo.h"
#include "capture.h"
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <sys/stat.h>
#include <vector>
struct DemoEntry {
const char* id; // id estable, apto para comparar seleccion
const char* label; // texto en sidebar
const char* category; // "Core" o "Viz"
void (*fn)(); // puntero a la demo_xxx
};
static const DemoEntry k_demos[] = {
// Core
{"button", "button", "Core", &gallery::demo_button},
{"icon_button", "icon_button", "Core", &gallery::demo_icon_button},
{"toolbar", "toolbar", "Core", &gallery::demo_toolbar},
{"modal_dialog", "modal_dialog", "Core", &gallery::demo_modal},
{"text_input", "text_input", "Core", &gallery::demo_text_input},
{"select", "select", "Core", &gallery::demo_select},
{"toast", "toast + inbox", "Core", &gallery::demo_toast},
{"tree_view", "tree_view", "Core", &gallery::demo_tree_view},
{"badge", "badge", "Core", &gallery::demo_badge},
{"empty_state", "empty_state", "Core", &gallery::demo_empty_state},
{"page_header", "page_header", "Core", &gallery::demo_page_header},
{"dashboard_panel", "dashboard_panel", "Core", &gallery::demo_dashboard_panel},
{"kpi_card", "kpi_card", "Core", &gallery::demo_kpi_card},
{"text_editor", "text_editor", "Core", &gallery::demo_text_editor}, // wave 1
{"file_watcher", "file_watcher", "Core", &gallery::demo_file_watcher}, // wave 1
{"process_runner", "process_runner", "Core", &gallery::demo_process_runner},
{"tween", "tween_curves", "Core", &gallery::demo_tween},
{"bezier_editor", "bezier_editor", "Core", &gallery::demo_bezier_editor},
{"timeline", "timeline", "Core", &gallery::demo_timeline},
{"sql_workbench", "sql_workbench", "Core", &gallery::demo_sql_workbench}, // issue 0032
// Viz
{"bar_chart", "bar_chart", "Viz", &gallery::demo_bar_chart},
{"pie_chart", "pie_chart", "Viz", &gallery::demo_pie_chart},
{"line_plot", "line_plot", "Viz", &gallery::demo_line_plot},
{"scatter_plot", "scatter_plot", "Viz", &gallery::demo_scatter_plot},
{"histogram", "histogram", "Viz", &gallery::demo_histogram},
{"sparkline", "sparkline", "Viz", &gallery::demo_sparkline},
{"graph_viewport", "graph_viewport", "Viz", &gallery::demo_graph},
{"graph_styles", "graph_styles", "Viz", &gallery::demo_graph_styles}, // issue 0049f
{"candlestick", "candlestick", "Viz", &gallery::demo_candlestick},
{"gauge", "gauge", "Viz", &gallery::demo_gauge},
{"heatmap", "heatmap", "Viz", &gallery::demo_heatmap},
{"table_view", "table_view", "Viz", &gallery::demo_table_view},
{"surface_plot_3d", "surface_plot_3d", "Viz", &gallery::demo_surface_plot_3d},
{"scatter_3d", "scatter_3d", "Viz", &gallery::demo_scatter_3d},
{"mesh_viewer", "mesh_viewer", "Viz", &gallery::demo_mesh_viewer},
{"treemap", "treemap", "Viz", &gallery::demo_treemap},
{"sankey", "sankey", "Viz", &gallery::demo_sankey},
{"chord", "chord", "Viz", &gallery::demo_chord},
{"contour", "contour", "Viz", &gallery::demo_contour},
{"voronoi", "voronoi", "Viz", &gallery::demo_voronoi},
// Gfx (shaders_lab core)
{"shader_canvas", "shader_canvas", "Gfx", &gallery::demo_shader_canvas},
{"gl_texture", "gl_texture_load", "Gfx", &gallery::demo_gl_texture}, // wave 1
{"gl_info", "gl_info", "Gfx", &gallery::demo_gl_info}, // issue 0049b
};
static constexpr int k_demo_count = sizeof(k_demos) / sizeof(k_demos[0]);
static std::string g_selected_id = "button";
static const DemoEntry* find_demo(const std::string& id) {
for (int i = 0; i < k_demo_count; i++) {
if (id == k_demos[i].id) return &k_demos[i];
}
return &k_demos[0];
}
static void draw_sidebar() {
ImGui::BeginChild("##gallery_sidebar", ImVec2(220, 0),
ImGuiChildFlags_Borders);
// Agrupar por categoria como rama del tree_view (categorias abiertas por
// defecto). Cada demo es una hoja seleccionable.
int i = 0;
while (i < k_demo_count) {
const char* category = k_demos[i].category;
// Default-open la rama la primera vez que se abre el sidebar.
ImGui::SetNextItemOpen(true, ImGuiCond_FirstUseEver);
if (fn_ui::tree_branch_begin(category, category, /*selected=*/false)) {
// Recorrer todas las demos consecutivas con esta misma categoria.
while (i < k_demo_count
&& std::strcmp(k_demos[i].category, category) == 0) {
const auto& d = k_demos[i];
const bool selected = (g_selected_id == d.id);
fn_ui::tree_leaf(d.id, d.label, selected);
if (fn_ui::tree_node_clicked()) {
g_selected_id = d.id;
}
i++;
}
fn_ui::tree_branch_end();
} else {
// Rama colapsada — saltar todos sus items.
while (i < k_demo_count
&& std::strcmp(k_demos[i].category, category) == 0) {
i++;
}
}
}
ImGui::EndChild();
}
static void render() {
// Theme y gl_loader gestionados por fn::run_app (theme=FnDark por defecto,
// init_gl_loader=true en AppConfig). Menubar via run_app.
// auto_dockspace=false porque usamos fullscreen_window que ocupa todo.
fullscreen_window_begin("##gallery");
page_header_begin("Primitives Gallery",
"Visual catalog of fn_registry C++ UI primitives");
page_header_end();
if (ImGui::BeginTable("##layout", 2,
ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingFixedFit)) {
ImGui::TableSetupColumn("sidebar", ImGuiTableColumnFlags_WidthFixed, 220.0f);
ImGui::TableSetupColumn("content", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
draw_sidebar();
ImGui::TableSetColumnIndex(1);
ImGui::BeginChild("##gallery_content", ImVec2(0, 0),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_HorizontalScrollbar);
// Cuando cambia el tamaño de fuente (Settings > Size), el contenido
// del child crece/encoge pero la posicion de scroll en pixeles
// no — efecto: lo visible "se baja". Escalamos scroll_y por el
// ratio de fuentes para mantener la misma linea logica arriba.
{
static float s_prev_font_size = 0.0f;
float cur_font_size = ImGui::GetStyle().FontSizeBase;
if (s_prev_font_size > 0.0f &&
std::fabs(s_prev_font_size - cur_font_size) > 0.01f) {
ImGui::SetScrollY(ImGui::GetScrollY() *
(cur_font_size / s_prev_font_size));
}
s_prev_font_size = cur_font_size;
}
const DemoEntry* d = find_demo(g_selected_id);
if (d && d->fn) d->fn();
ImGui::EndChild();
ImGui::EndTable();
}
fullscreen_window_end();
// Toasts se renderizan encima para que el demo de toast funcione aqui tambien.
fn_ui::toast_render();
}
int main(int argc, char** argv) {
// Capture mode: `primitives_gallery --capture <output_dir>` corre cada
// demo en una ventana GLFW invisible y guarda PNG por demo. Para CI/golden.
for (int i = 1; i < argc; i++) {
if (std::strcmp(argv[i], "--capture") == 0) {
if (i + 1 >= argc) {
std::fprintf(stderr, "--capture requires an output dir argument\n");
return 2;
}
const char* out_dir = argv[i + 1];
// Best-effort mkdir (idempotente). Windows mkdir() solo acepta el path.
#if defined(_WIN32)
mkdir(out_dir);
#else
mkdir(out_dir, 0755);
#endif
std::vector<gallery::CaptureItem> items;
items.reserve(k_demo_count);
for (int j = 0; j < k_demo_count; j++) {
items.push_back({k_demos[j].id, k_demos[j].fn});
}
gallery::CaptureConfig cfg;
cfg.output_dir = out_dir;
cfg.warmup_frames = 3;
cfg.capture_w = 800;
cfg.capture_h = 600;
const bool ok = gallery::run_capture(cfg, items);
return ok ? 0 : 1;
}
}
return fn::run_app(
{.title = "fn_registry · Primitives Gallery",
.width = 1400,
.height = 900,
.viewports = true,
.about = {.name = "Primitives Gallery",
.version = "0.4.0",
.description = "Visual catalog of fn_registry C++ UI primitives. Now on OpenGL 4.3 core (compute, SSBOs, image load/store) — ver demo gl_info."},
.init_gl_loader = true,
.auto_dockspace = false,
.log = {"primitives_gallery.log", 1}},
render
);
}
+6
View File
@@ -0,0 +1,6 @@
# Tables playground - vive dentro de primitives_gallery/ (playgrounds.md).
# No es un app del registry: no tiene app.md, no se indexa.
add_imgui_app(tables_playground
main.cpp
${CMAKE_SOURCE_DIR}/functions/viz/table_view.cpp
)
+115
View File
@@ -0,0 +1,115 @@
// 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++.
#include "app_base.h"
#include "imgui.h"
#include "viz/table_view.h"
#include "core/logger.h"
#include <cstdio>
#include <string>
#include <vector>
namespace {
struct Row {
const char* name;
const char* lang;
const char* domain;
const char* purity;
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"},
};
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) {
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) {
flat.push_back(r.name);
flat.push_back(r.lang);
flat.push_back(r.domain);
flat.push_back(r.purity);
flat.push_back(r.description);
}
built = true;
}
out_rows = static_cast<int>(sample_rows().size());
out_cols = 5;
return flat.data();
}
} // namespace
void render() {
if (ImGui::Begin("Tables Playground - table_view actual")) {
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.");
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);
}
ImGui::End();
}
#ifndef FN_TEST_BUILD
int main() {
return fn::run_app({
.title = "Tables Playground",
.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++."},
.log = {.file_path = "tables_playground.log",
.level = static_cast<int>(fn_log::Level::Info)}
}, render);
}
#endif