From 087412d73af258530884fa7a7010614fa0bbae2d Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 25 Apr 2026 21:00:55 +0200 Subject: [PATCH] feat(primitives_gallery): wire text_editor + file_watcher demo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - demos_text_editor.cpp: split horizontal con editor GLSL precargado a la izquierda (boton Save to /tmp/fn_demo.glsl + dirty indicator) y panel de eventos a la derecha (path, active flag, lista scrollable, boton clear). Watcher activo sobre /tmp/fn_demo.glsl; reintenta el add() tras el primer Save si el archivo no existia al iniciar. - demos.h: declaracion de gallery::demo_text_editor() - main.cpp: entry "text_editor"/"text_editor + watcher" en categoria Core - CMakeLists.txt: anade demos_text_editor.cpp + sources de text_editor, file_watcher y vendor TextEditor.cpp + include path de imgui_text_edit Nota: la primitives_gallery NO se construye en este branch (sus deps — button.cpp, toolbar.cpp, etc. — son untracked en master). El subdirectorio se anade pero protegido por FN_BUILD_GALLERY=OFF para no romper builds. Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/apps/primitives_gallery/CMakeLists.txt | 60 +++ cpp/apps/primitives_gallery/README.md | 159 +++++++ cpp/apps/primitives_gallery/demo.cpp | 76 +++ cpp/apps/primitives_gallery/demo.h | 22 + cpp/apps/primitives_gallery/demos.h | 37 ++ cpp/apps/primitives_gallery/demos_core.cpp | 447 ++++++++++++++++++ cpp/apps/primitives_gallery/demos_gfx.cpp | 123 +++++ cpp/apps/primitives_gallery/demos_graph.cpp | 204 ++++++++ .../primitives_gallery/demos_text_editor.cpp | 219 +++++++++ cpp/apps/primitives_gallery/demos_viz.cpp | 211 +++++++++ cpp/apps/primitives_gallery/main.cpp | 159 +++++++ 11 files changed, 1717 insertions(+) create mode 100644 cpp/apps/primitives_gallery/CMakeLists.txt create mode 100644 cpp/apps/primitives_gallery/README.md create mode 100644 cpp/apps/primitives_gallery/demo.cpp create mode 100644 cpp/apps/primitives_gallery/demo.h create mode 100644 cpp/apps/primitives_gallery/demos.h create mode 100644 cpp/apps/primitives_gallery/demos_core.cpp create mode 100644 cpp/apps/primitives_gallery/demos_gfx.cpp create mode 100644 cpp/apps/primitives_gallery/demos_graph.cpp create mode 100644 cpp/apps/primitives_gallery/demos_text_editor.cpp create mode 100644 cpp/apps/primitives_gallery/demos_viz.cpp create mode 100644 cpp/apps/primitives_gallery/main.cpp diff --git a/cpp/apps/primitives_gallery/CMakeLists.txt b/cpp/apps/primitives_gallery/CMakeLists.txt new file mode 100644 index 00000000..7fb2bae9 --- /dev/null +++ b/cpp/apps/primitives_gallery/CMakeLists.txt @@ -0,0 +1,60 @@ +add_imgui_app(primitives_gallery + main.cpp + demo.cpp + demos_core.cpp + demos_viz.cpp + demos_graph.cpp + demos_gfx.cpp + demos_text_editor.cpp + # text_editor + file_watcher (issue 0025) + ${CMAKE_SOURCE_DIR}/functions/core/text_editor.cpp + ${CMAKE_SOURCE_DIR}/functions/core/file_watcher.cpp + ${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit/TextEditor.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 + # 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 + # 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_force_layout.cpp + ${CMAKE_SOURCE_DIR}/functions/viz/graph_viewport.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 +) +target_include_directories(primitives_gallery PRIVATE + ${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit +) + +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/cpp/apps/primitives_gallery/README.md b/cpp/apps/primitives_gallery/README.md new file mode 100644 index 00000000..59e6fec4 --- /dev/null +++ b/cpp/apps/primitives_gallery/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/cpp/apps/primitives_gallery/demo.cpp b/cpp/apps/primitives_gallery/demo.cpp new file mode 100644 index 00000000..d2ab17a0 --- /dev/null +++ b/cpp/apps/primitives_gallery/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/cpp/apps/primitives_gallery/demo.h b/cpp/apps/primitives_gallery/demo.h new file mode 100644 index 00000000..94177743 --- /dev/null +++ b/cpp/apps/primitives_gallery/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/cpp/apps/primitives_gallery/demos.h b/cpp/apps/primitives_gallery/demos.h new file mode 100644 index 00000000..4fe67342 --- /dev/null +++ b/cpp/apps/primitives_gallery/demos.h @@ -0,0 +1,37 @@ +#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(); + +// --- 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(); + +// --- Gfx --- +void demo_shader_canvas(); + +// --- Core (combined demo: text_editor + file_watcher) --- +void demo_text_editor(); + +} // namespace gallery diff --git a/cpp/apps/primitives_gallery/demos_core.cpp b/cpp/apps/primitives_gallery/demos_core.cpp new file mode 100644 index 00000000..3d2566bd --- /dev/null +++ b/cpp/apps/primitives_gallery/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/cpp/apps/primitives_gallery/demos_gfx.cpp b/cpp/apps/primitives_gallery/demos_gfx.cpp new file mode 100644 index 00000000..60dcf6a6 --- /dev/null +++ b/cpp/apps/primitives_gallery/demos_gfx.cpp @@ -0,0 +1,123 @@ +// 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);" + ); +} + +} // namespace gallery diff --git a/cpp/apps/primitives_gallery/demos_graph.cpp b/cpp/apps/primitives_gallery/demos_graph.cpp new file mode 100644 index 00000000..2311d431 --- /dev/null +++ b/cpp/apps/primitives_gallery/demos_graph.cpp @@ -0,0 +1,204 @@ +#include "demos.h" +#include "demo.h" + +#include "viz/graph_types.h" +#include "viz/graph_viewport.h" +#include "viz/graph_force_layout.h" +#include "core/button.h" +#include "core/tokens.h" + +#include +#include +#include +#include + +namespace gallery { + +// Genera un grafo sintetico con N nodos en K clusters + aristas intra-cluster +// y unas pocas inter-cluster. Pensado para demostrar el rendimiento del +// pipeline graph_renderer + graph_force_layout + graph_viewport. +static void generate_synthetic_graph(int N, int K, + std::vector& nodes_out, + std::vector& edges_out) { + nodes_out.clear(); + edges_out.clear(); + nodes_out.reserve(N); + edges_out.reserve(N * 3); + + unsigned seed = 0x1234abcd; + auto rnd = [&]() { + seed = seed * 1664525u + 1013904223u; + return static_cast((seed >> 8) & 0xffffff) / 16777216.0f; + }; + + // Paleta por cluster (ABGR) + const uint32_t palette[] = { + 0xff5b8def, 0xff58ca8c, 0xfff5973e, 0xffd95150, + 0xffb87fe0, 0xff5fcdcc, 0xfff2cd52, 0xff99d161, + }; + const int palette_n = sizeof(palette) / sizeof(palette[0]); + + // Asignar cluster + posicion inicial cerca del centroide del cluster + 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) * 200.0f; + cluster_cy[k] = std::sin(angle) * 200.0f; + } + + for (int i = 0; i < N; i++) { + int k = i % K; + GraphNode n = graph_node(static_cast(i), + cluster_cx[k] + (rnd() - 0.5f) * 80.0f, + cluster_cy[k] + (rnd() - 0.5f) * 80.0f); + n.size = 3.0f + rnd() * 2.0f; + n.color = palette[k % palette_n]; + n.community = static_cast(k); + nodes_out.push_back(n); + } + + // Aristas: ~3 por nodo dentro del cluster, +5% inter-cluster. + 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; + // Dentro del cluster + for (int i = base; i < end; i++) { + for (int e = 0; e < 3; e++) { + int j = base + static_cast(rnd() * size); + add_edge(static_cast(i), + static_cast(j), 1.0f); + } + } + } + // Inter-cluster (5% de los nodos) + int inter = N / 20; + for (int 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 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; + + if (s_needs_regen) { + generate_synthetic_graph(s_n_nodes, s_n_clusters, s_nodes, s_edges); + s_graph.nodes = s_nodes.data(); + s_graph.node_count = static_cast(s_nodes.size()); + s_graph.edges = s_edges.data(); + s_graph.edge_count = static_cast(s_edges.size()); + s_graph.update_bounds(); + s_state.layout_running = true; + s_state.layout_energy = 0.0f; + s_needs_regen = false; + s_initialized = true; + } + + section("Controls"); + { + using namespace fn_ui; + // Sliders en dos filas para que quepan sin scrollbar + ImGui::PushItemWidth(180); + ImGui::SliderInt("Nodes", &s_n_nodes, 100, 20000); + ImGui::SameLine(); + ImGui::SliderInt("Clusters", &s_n_clusters, 2, 16); + 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); + } + } + + 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 c%u", + s_state.hovered_node, + s_nodes[s_state.hovered_node].community); + } 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(); + } + + section("Viewport (drag = pan, wheel = zoom, click = select)"); + if (s_initialized) { + // Avanzamos 1 paso de force layout cada frame mientras layout_running + if (s_state.layout_running) { + ForceLayoutConfig cfg; + cfg.repulsion = s_repulsion; + cfg.attraction = s_attraction; + cfg.gravity = s_gravity; + cfg.iterations = 1; + s_state.layout_energy = graph_force_layout_step(s_graph, cfg); + } + graph_viewport("##graph_demo", s_graph, s_state, ImVec2(0, 460)); + } + + 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/cpp/apps/primitives_gallery/demos_text_editor.cpp b/cpp/apps/primitives_gallery/demos_text_editor.cpp new file mode 100644 index 00000000..36c4c895 --- /dev/null +++ b/cpp/apps/primitives_gallery/demos_text_editor.cpp @@ -0,0 +1,219 @@ +// Demo combinada: text_editor + file_watcher. +// +// Layout (split horizontal): +// - Izquierda: text_editor con CodeLang::GLSL precargado con un fragment +// shader simple. Boton "Save to /tmp/fn_demo.glsl". +// - Derecha: panel de info — dirty flag, ultimo error, lista scrollable de +// eventos del watcher activo sobre /tmp/fn_demo.glsl. + +#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 { + +namespace { + +constexpr const char* kDemoPath = "/tmp/fn_demo.glsl"; + +const char* kInitialGLSL = + "#version 330\n" + "// Demo fragment shader (text_editor + file_watcher).\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"; + +struct EventLogEntry { + double t_seconds; // tiempo relativo al primer evento mostrado + std::string label; +}; + +struct DemoState { + fn::TextEditorState* editor = nullptr; + fn::FileWatcher* watcher = nullptr; + std::deque events; + std::string save_status; + std::string watch_error; + bool watcher_active = false; +}; + +DemoState& state() { + static DemoState s; + return s; +} + +void ensure_init() { + auto& s = state(); + if (!s.editor) { + s.editor = fn::text_editor_create(fn::CodeLang::GLSL); + fn::text_editor_set_text(s.editor, kInitialGLSL); + } + if (!s.watcher) { + s.watcher = fn::file_watcher_create(); + // Si /tmp/fn_demo.glsl no existe aun, file_watcher_add fallara — + // se reintenta tras el primer Save. + s.watcher_active = fn::file_watcher_add(s.watcher, kDemoPath); + if (!s.watcher_active) { + s.watch_error = fn::file_watcher_last_error(s.watcher); + } + } +} + +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 = state(); + if (!s.watcher) return; + auto evs = fn::file_watcher_poll(s.watcher); + if (evs.empty()) return; + double now = (double)std::time(nullptr); + 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({now, buf}); + } + while (s.events.size() > 200) s.events.pop_front(); +} + +bool save_to_disk() { + auto& s = state(); + FILE* f = std::fopen(kDemoPath, "w"); + if (!f) { + s.save_status = std::string("save failed: ") + std::strerror(errno); + return false; + } + const char* txt = fn::text_editor_get_text(s.editor); + std::fputs(txt, f); + std::fclose(f); + fn::text_editor_clear_dirty(s.editor); + s.save_status = std::string("saved -> ") + kDemoPath; + + // Si el watcher no estaba activo (archivo no existia al iniciar), reintentar. + if (!s.watcher_active) { + s.watcher_active = fn::file_watcher_add(s.watcher, kDemoPath); + if (!s.watcher_active) s.watch_error = fn::file_watcher_last_error(s.watcher); + else s.watch_error.clear(); + } + return true; +} + +} // namespace + +void demo_text_editor() { + using namespace fn_tokens; + + demo_header("text_editor + file_watcher", "v1.0.0", + "Editor de codigo GLSL con syntax highlighting (PIMPL sobre ImGuiColorTextEdit) " + "+ watcher de archivos no bloqueante (inotify Linux / ReadDirectoryChangesW Win). " + "Edita, pulsa Save y observa el evento llegar al panel derecho."); + + ensure_init(); + poll_and_log(); + + auto& s = state(); + + // Layout: two-column table. Editor a la izquierda, info a la derecha. + if (ImGui::BeginTable("##te_layout", 2, + ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp)) { + ImGui::TableSetupColumn("editor", ImGuiTableColumnFlags_WidthStretch, 0.62f); + ImGui::TableSetupColumn("info", ImGuiTableColumnFlags_WidthStretch, 0.38f); + ImGui::TableNextRow(); + + // ---------- Columna izquierda: editor ---------- + ImGui::TableSetColumnIndex(0); + + section("editor (CodeLang::GLSL)"); + + ImVec2 avail = ImGui::GetContentRegionAvail(); + float editor_h = avail.y - 60.0f; + if (editor_h < 200.0f) editor_h = 200.0f; + fn::text_editor_render(s.editor, "##fn_text_editor", ImVec2(-1, editor_h)); + + ImGui::Spacing(); + if (fn_ui::button("Save to /tmp/fn_demo.glsl", fn_ui::ButtonVariant::Primary)) { + save_to_disk(); + } + ImGui::SameLine(); + if (fn::text_editor_is_dirty(s.editor)) { + ImGui::PushStyleColor(ImGuiCol_Text, colors::warning); + ImGui::TextUnformatted("(modified)"); + ImGui::PopStyleColor(); + } else { + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim); + ImGui::TextUnformatted("(clean)"); + ImGui::PopStyleColor(); + } + + if (!s.save_status.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim); + ImGui::TextUnformatted(s.save_status.c_str()); + ImGui::PopStyleColor(); + } + + // ---------- Columna derecha: info + eventos ---------- + ImGui::TableSetColumnIndex(1); + + section("watcher state"); + + ImGui::Text("path: %s", kDemoPath); + ImGui::Text("active: %s", s.watcher_active ? "yes" : "no"); + + if (!s.watch_error.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, colors::error); + ImGui::TextWrapped("err: %s", s.watch_error.c_str()); + ImGui::PopStyleColor(); + } + + ImGui::Spacing(); + section("events"); + + ImGui::Text("captured: %d", (int)s.events.size()); + ImGui::SameLine(); + if (fn_ui::button("clear##evlog", fn_ui::ButtonVariant::Subtle, fn_ui::ButtonSize::Sm)) { + s.events.clear(); + } + + ImGui::BeginChild("##evlog", ImVec2(0, 0), ImGuiChildFlags_Borders); + if (s.events.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim); + ImGui::TextWrapped("Sin eventos. Modifica el editor + Save, " + "o desde otro terminal: echo hi >> %s", kDemoPath); + ImGui::PopStyleColor(); + } else { + for (auto it = s.events.rbegin(); it != s.events.rend(); ++it) { + ImGui::TextUnformatted(it->label.c_str()); + } + } + ImGui::EndChild(); + + ImGui::EndTable(); + } +} + +} // namespace gallery diff --git a/cpp/apps/primitives_gallery/demos_viz.cpp b/cpp/apps/primitives_gallery/demos_viz.cpp new file mode 100644 index 00000000..a5b81117 --- /dev/null +++ b/cpp/apps/primitives_gallery/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/cpp/apps/primitives_gallery/main.cpp b/cpp/apps/primitives_gallery/main.cpp new file mode 100644 index 00000000..cba55d8d --- /dev/null +++ b/cpp/apps/primitives_gallery/main.cpp @@ -0,0 +1,159 @@ +// 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 "gfx/gl_loader.h" + +#include "demos.h" +#include "demo.h" + +#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 + watcher", "Core", &gallery::demo_text_editor}, + // 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}, + // Gfx (shaders_lab core) + {"shader_canvas", "shader_canvas", "Gfx", &gallery::demo_shader_canvas}, +}; +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() { + using namespace fn_tokens; + ImGui::BeginChild("##gallery_sidebar", ImVec2(220, 0), + ImGuiChildFlags_Borders); + + const char* current_category = nullptr; + for (int i = 0; i < k_demo_count; i++) { + const auto& d = k_demos[i]; + if (!current_category || std::strcmp(current_category, d.category) != 0) { + if (current_category) ImGui::Dummy(ImVec2(0, spacing::sm)); + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim); + ImGui::TextUnformatted(d.category); + ImGui::PopStyleColor(); + ImGui::Separator(); + current_category = d.category; + } + + const bool selected = (g_selected_id == d.id); + ImGui::PushStyleColor(ImGuiCol_Header, selected ? colors::surface_hover : ImVec4(0,0,0,0)); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, colors::surface_hover); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, colors::surface); + ImGui::PushStyleColor(ImGuiCol_Text, selected ? colors::primary : colors::text); + + char label[96]; + std::snprintf(label, sizeof(label), "%s##sel_%s", d.label, d.id); + if (ImGui::Selectable(label, selected)) { + g_selected_id = d.id; + } + + ImGui::PopStyleColor(4); + } + + ImGui::EndChild(); +} + +static void render() { + static bool init_done = false; + if (!init_done) { + fn_tokens::apply_dark_theme(); + // En Linux es no-op; en Windows resuelve los punteros GL via wglGetProcAddress. + // Imprescindible antes de invocar primitivos que usen OpenGL 2.0+ (graph_viewport, + // shader_canvas, etc). + fn::gfx::gl_loader_init(); + init_done = true; + } + + // MainMenuBar (solo Settings — la gallery no tiene paneles toggleables ni layouts) + fn_ui::app_menubar(nullptr, 0, nullptr); + + 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); + 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*/) { + return fn::run_app( + {.title = "fn_registry · Primitives Gallery", + .width = 1400, .height = 900, .viewports = true}, + render + ); +}