From f1e2c1cd19be7fbb65d52c800d198178b40363f4 Mon Sep 17 00:00:00 2001 From: fn-registry agent Date: Mon, 11 May 2026 16:28:44 +0200 Subject: [PATCH] chore: sync from fn-registry agent --- CMakeLists.txt | 110 ++++++++ README.md | 159 +++++++++++ app.md | 37 +++ assets/sample.png | Bin 0 -> 966 bytes capture.cpp | 173 ++++++++++++ capture.h | 34 +++ demo.cpp | 76 ++++++ demo.h | 22 ++ demos.h | 56 ++++ demos_3d.cpp | 100 +++++++ demos_animation.cpp | 249 +++++++++++++++++ demos_core.cpp | 447 +++++++++++++++++++++++++++++++ demos_extras.cpp | 215 +++++++++++++++ demos_gfx.cpp | 196 ++++++++++++++ demos_gl_texture.cpp | 127 +++++++++ demos_graph.cpp | 443 ++++++++++++++++++++++++++++++ demos_graph_styles.cpp | 243 +++++++++++++++++ demos_mesh.cpp | 108 ++++++++ demos_scientific.cpp | 208 ++++++++++++++ demos_sql.cpp | 129 +++++++++ demos_text_editor.cpp | 279 +++++++++++++++++++ demos_viz.cpp | 211 +++++++++++++++ main.cpp | 230 ++++++++++++++++ playground/tables/CMakeLists.txt | 6 + playground/tables/main.cpp | 115 ++++++++ 25 files changed, 3973 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 app.md create mode 100644 assets/sample.png create mode 100644 capture.cpp create mode 100644 capture.h create mode 100644 demo.cpp create mode 100644 demo.h create mode 100644 demos.h create mode 100644 demos_3d.cpp create mode 100644 demos_animation.cpp create mode 100644 demos_core.cpp create mode 100644 demos_extras.cpp create mode 100644 demos_gfx.cpp create mode 100644 demos_gl_texture.cpp create mode 100644 demos_graph.cpp create mode 100644 demos_graph_styles.cpp create mode 100644 demos_mesh.cpp create mode 100644 demos_scientific.cpp create mode 100644 demos_sql.cpp create mode 100644 demos_text_editor.cpp create mode 100644 demos_viz.cpp create mode 100644 main.cpp create mode 100644 playground/tables/CMakeLists.txt create mode 100644 playground/tables/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c71a213 --- /dev/null +++ b/CMakeLists.txt @@ -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() diff --git a/README.md b/README.md new file mode 100644 index 0000000..59e6fec --- /dev/null +++ b/README.md @@ -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. diff --git a/app.md b/app.md new file mode 100644 index 0000000..aca75af --- /dev/null +++ b/app.md @@ -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 +``` + +Renderiza cada demo offscreen y guarda PNGs en `/`. 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). diff --git a/assets/sample.png b/assets/sample.png new file mode 100644 index 0000000000000000000000000000000000000000..5156f39c3e7739cb4561556a74d9788fbe28482b GIT binary patch literal 966 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|GzJFdUQZXtkcwMxuP?lM+kl}p zaJf2j3XgODhkk_&35kph5R#BsA;`qdYwNTiA#zt!x7>@p+%~d}?>&~U`~9=baqZqo zpJR9ST>N`W?&9CCMcwcGb35PVPq*ARi8{ zws8CY`nTZS_~X$f^;P;8|K`qF{QI!=?)R(=`zZ4c*sk`ux9;sb$;{*38F#?Ue(~?i zMZNFVf0nwt{f(}m zjhnUl3?En-SQrEt96$+#T!zJ)D|OrRKQSDRTO_{WD=?zA#4rAxy&o8kcCO#O>-*nr zx20b2?fQP+yt3ii-bLa8yMUhB?Osy(jQTKZD?>L4B++-(iXv*VYD7X>tw + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include "stb_image_write.h" + +#include +#include + +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 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& 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 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 diff --git a/capture.h b/capture.h new file mode 100644 index 0000000..10f5bc6 --- /dev/null +++ b/capture.h @@ -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/.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 +#include + +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& items); + +} // namespace gallery diff --git a/demo.cpp b/demo.cpp new file mode 100644 index 0000000..d2ab17a --- /dev/null +++ b/demo.cpp @@ -0,0 +1,76 @@ +#include "demo.h" +#include "core/tokens.h" +#include + +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 diff --git a/demo.h b/demo.h new file mode 100644 index 0000000..9417774 --- /dev/null +++ b/demo.h @@ -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 + +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 diff --git a/demos.h b/demos.h new file mode 100644 index 0000000..8e460f3 --- /dev/null +++ b/demos.h @@ -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 diff --git a/demos_3d.cpp b/demos_3d.cpp new file mode 100644 index 0000000..30f8c50 --- /dev/null +++ b/demos_3d.cpp @@ -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 +#include +#include +#include + +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", &, 0.1f, 3.0f, "%.2f"); + + constexpr int N = 64; + static std::vector 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 xs(N), ys(N), zs(N); + static std::vector colors(N); + static bool initialized = false; + + if (!initialized) { + std::mt19937 rng(42); + std::normal_distribution 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 diff --git a/demos_animation.cpp b/demos_animation.cpp new file mode 100644 index 0000000..877324a --- /dev/null +++ b/demos_animation.cpp @@ -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 + +#include +#include + +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 diff --git a/demos_core.cpp b/demos_core.cpp new file mode 100644 index 0000000..3d2566b --- /dev/null +++ b/demos_core.cpp @@ -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 +#include + +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 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 diff --git a/demos_extras.cpp b/demos_extras.cpp new file mode 100644 index 0000000..dec8ace --- /dev/null +++ b/demos_extras.cpp @@ -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 +#include +#include +#include +#include +#include + +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 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 diff --git a/demos_gfx.cpp b/demos_gfx.cpp new file mode 100644 index 0000000..b69e50a --- /dev/null +++ b/demos_gfx.cpp @@ -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 +#include + +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( + 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(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 diff --git a/demos_gl_texture.cpp b/demos_gl_texture.cpp new file mode 100644 index 0000000..0d7e05c --- /dev/null +++ b/demos_gl_texture.cpp @@ -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 +#include +#include + +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 diff --git a/demos_graph.cpp b/demos_graph.cpp new file mode 100644 index 0000000..11cce7a --- /dev/null +++ b/demos_graph.cpp @@ -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 +#include +#include +#include + +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& nodes_out, + std::vector& 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((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(std::max(N, 1))); + const float cluster_r = 12.0f * scale; + const float scatter = 4.0f * scale; + + std::vector 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(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(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(rnd() * size); + add_edge(static_cast(i), + static_cast(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(rnd() * N); + uint32_t b = static_cast(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 s_nodes; + static std::vector 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(s_nodes.size()); + s_graph.node_capacity = static_cast(s_nodes.capacity()); + s_graph.edges = s_edges.data(); + s_graph.edge_count = static_cast(s_edges.size()); + s_graph.edge_capacity = static_cast(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 "#" 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(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 diff --git a/demos_graph_styles.cpp b/demos_graph_styles.cpp new file mode 100644 index 0000000..917f825 --- /dev/null +++ b/demos_graph_styles.cpp @@ -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 +#include +#include +#include + +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& nodes, + std::vector& 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 s_nodes; + static std::vector 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 diff --git a/demos_mesh.cpp b/demos_mesh.cpp new file mode 100644 index 0000000..5626f35 --- /dev/null +++ b/demos_mesh.cpp @@ -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 +#include +#include + +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 diff --git a/demos_scientific.cpp b/demos_scientific.cpp new file mode 100644 index 0000000..632b6c9 --- /dev/null +++ b/demos_scientific.cpp @@ -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 +#include +#include +#include + +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 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 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 nodes = { + {"premium"}, {"basicos"}, + {"laptops"}, {"phones"}, {"tablets"}, + {"hardware"}, {"software"}, {"servicios"}, + }; + std::vector 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 nodes = {{\"premium\"}, {\"basicos\"}, ...};\n" + "std::vector 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 diff --git a/demos_sql.cpp b/demos_sql.cpp new file mode 100644 index 0000000..01dc4a3 --- /dev/null +++ b/demos_sql.cpp @@ -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 +#include + +#include +#include +#include +#include + +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 diff --git a/demos_text_editor.cpp b/demos_text_editor.cpp new file mode 100644 index 0000000..c6a32f4 --- /dev/null +++ b/demos_text_editor.cpp @@ -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 + +#include +#include +#include +#include +#include +#include + +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 \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 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 diff --git a/demos_viz.cpp b/demos_viz.cpp new file mode 100644 index 0000000..a5b8111 --- /dev/null +++ b/demos_viz.cpp @@ -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 +#include +#include + +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(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((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((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 diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..22c76f6 --- /dev/null +++ b/main.cpp @@ -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 +#include +#include +#include +#include +#include + +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 ` 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 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 + ); +} diff --git a/playground/tables/CMakeLists.txt b/playground/tables/CMakeLists.txt new file mode 100644 index 0000000..5040f6f --- /dev/null +++ b/playground/tables/CMakeLists.txt @@ -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 +) diff --git a/playground/tables/main.cpp b/playground/tables/main.cpp new file mode 100644 index 0000000..52123a8 --- /dev/null +++ b/playground/tables/main.cpp @@ -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 +#include +#include + +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& sample_rows() { + static const std::vector 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 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(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(fn_log::Level::Info)} + }, render); +} +#endif