fix(infra): gradle_run detecta android-sdk — issue 0076 #2
@@ -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()
|
||||
@@ -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.
|
||||
@@ -0,0 +1,76 @@
|
||||
#include "demo.h"
|
||||
#include "core/tokens.h"
|
||||
#include <cstdio>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
void demo_header(const char* name, const char* version, const char* description) {
|
||||
using namespace fn_tokens;
|
||||
|
||||
ImGui::SetWindowFontScale(1.4f);
|
||||
ImGui::TextUnformatted(name);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
|
||||
ImGui::Text(" %s", version);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
if (description && *description) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
|
||||
ImGui::TextWrapped("%s", description);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::Separator();
|
||||
ImGui::Dummy(ImVec2(0, spacing::sm));
|
||||
}
|
||||
|
||||
void section(const char* title) {
|
||||
using namespace fn_tokens;
|
||||
ImGui::Dummy(ImVec2(0, spacing::sm));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
|
||||
ImGui::TextUnformatted(title);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Separator();
|
||||
ImGui::Dummy(ImVec2(0, spacing::xs));
|
||||
}
|
||||
|
||||
void variant_label(const char* text) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim);
|
||||
ImGui::TextUnformatted(text);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
void code_block(const char* code) {
|
||||
using namespace fn_tokens;
|
||||
ImGui::Dummy(ImVec2(0, spacing::sm));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
|
||||
ImGui::TextUnformatted("// example");
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, colors::bg);
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, colors::border);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, radius::sm);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::md, spacing::sm));
|
||||
|
||||
// Altura: aprox lineas * line-height
|
||||
int lines = 1;
|
||||
for (const char* p = code; *p; ++p) if (*p == '\n') ++lines;
|
||||
float h = lines * ImGui::GetTextLineHeightWithSpacing() + spacing::md;
|
||||
|
||||
char id[32];
|
||||
std::snprintf(id, sizeof(id), "##code_%p", (const void*)code);
|
||||
ImGui::BeginChild(id, ImVec2(0, h),
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text);
|
||||
ImGui::TextUnformatted(code);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::PopStyleVar(3);
|
||||
ImGui::PopStyleColor(2);
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
// Helpers compartidos por todas las demos de la gallery.
|
||||
// No son primitivos del registry — son utilidades locales de este app.
|
||||
|
||||
#include "imgui.h"
|
||||
#include <string>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
// Titulo + version + descripcion en la parte superior del panel derecho.
|
||||
void demo_header(const char* name, const char* version, const char* description);
|
||||
|
||||
// Seccion secundaria dentro de una demo (agrupar variantes).
|
||||
void section(const char* title);
|
||||
|
||||
// Bloque de codigo monoespaciado con bg surface y label "// example".
|
||||
void code_block(const char* code);
|
||||
|
||||
// Etiqueta sutil encima de un grupo de widgets.
|
||||
void variant_label(const char* text);
|
||||
|
||||
} // namespace gallery
|
||||
@@ -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
|
||||
@@ -0,0 +1,447 @@
|
||||
#include "demos.h"
|
||||
#include "demo.h"
|
||||
|
||||
#include "core/button.h"
|
||||
#include "core/icon_button.h"
|
||||
#include "core/toolbar.h"
|
||||
#include "core/modal_dialog.h"
|
||||
#include "core/text_input.h"
|
||||
#include "core/select.h"
|
||||
#include "core/toast.h"
|
||||
#include "core/tree_view.h"
|
||||
#include "core/badge.h"
|
||||
#include "core/empty_state.h"
|
||||
#include "core/page_header.h"
|
||||
#include "core/dashboard_panel.h"
|
||||
#include "core/tokens.h"
|
||||
#include "core/icons_tabler.h"
|
||||
#include "viz/kpi_card.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <cstdio>
|
||||
|
||||
using namespace fn_ui;
|
||||
using V = ButtonVariant;
|
||||
using S = ButtonSize;
|
||||
|
||||
namespace gallery {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// button
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_button() {
|
||||
demo_header("button", "v1.0.0",
|
||||
"Boton con 4 variantes semanticas y 3 tamanos. Usa tokens para colores, "
|
||||
"radius y padding — estilo consistente en toda la app.");
|
||||
|
||||
section("Variants x Sizes");
|
||||
const V variants[] = {V::Primary, V::Secondary, V::Subtle, V::Danger};
|
||||
const char* variant_names[] = {"Primary", "Secondary", "Subtle", "Danger"};
|
||||
const S sizes[] = {S::Sm, S::Md, S::Lg};
|
||||
const char* size_names[] = {"sm", "md", "lg"};
|
||||
|
||||
if (ImGui::BeginTable("##btn_grid", 5, ImGuiTableFlags_SizingFixedFit)) {
|
||||
ImGui::TableSetupColumn("size");
|
||||
for (int c = 0; c < 4; c++) ImGui::TableSetupColumn(variant_names[c]);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
for (int s = 0; s < 3; s++) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
variant_label(size_names[s]);
|
||||
for (int v = 0; v < 4; v++) {
|
||||
ImGui::TableSetColumnIndex(v + 1);
|
||||
char id[32];
|
||||
std::snprintf(id, sizeof(id), "%s##%d%d", variant_names[v], s, v);
|
||||
button(id, variants[v], sizes[s]);
|
||||
}
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
code_block(
|
||||
"#include \"core/button.h\"\n"
|
||||
"using fn_ui::button;\n"
|
||||
"using V = fn_ui::ButtonVariant;\n\n"
|
||||
"if (button(\"Save\", V::Primary)) save();\n"
|
||||
"if (button(\"Cancel\", V::Subtle)) close();\n"
|
||||
"if (button(\"Delete\", V::Danger)) confirm();"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// icon_button
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_icon_button() {
|
||||
demo_header("icon_button", "v1.0.0",
|
||||
"Boton cuadrado 28x28 con un glyph centrado y tooltip opcional. "
|
||||
"Usa los TI_* de core/icons_tabler.h (Tabler Icons cargado automaticamente "
|
||||
"por fn::run_app via icon_font.cpp).");
|
||||
|
||||
section("Tabler icon set");
|
||||
struct { const char* id; const char* glyph; const char* tip; } ic[] = {
|
||||
{"##rl", TI_REFRESH, "Reload"},
|
||||
{"##ad", TI_PLUS, "Add"},
|
||||
{"##dl", TI_TRASH, "Delete"},
|
||||
{"##dn", TI_CHEVRON_DOWN, "Dropdown"},
|
||||
{"##cf", TI_SETTINGS, "Settings"},
|
||||
{"##ok", TI_CHECK, "Check"},
|
||||
{"##cl", TI_X, "Close"},
|
||||
{"##ed", TI_PENCIL, "Edit"},
|
||||
{"##sv", TI_DEVICE_FLOPPY, "Save"},
|
||||
{"##sr", TI_SEARCH, "Search"},
|
||||
{"##hp", TI_HELP, "Help"},
|
||||
{"##hm", TI_HOME, "Home"},
|
||||
};
|
||||
for (auto& b : ic) {
|
||||
icon_button(b.id, b.glyph, b.tip);
|
||||
ImGui::SameLine();
|
||||
}
|
||||
ImGui::NewLine();
|
||||
|
||||
code_block(
|
||||
"#include \"core/icons_tabler.h\"\n\n"
|
||||
"if (icon_button(\"##reload\", TI_REFRESH, \"Reload\"))\n"
|
||||
" reload_data();\n\n"
|
||||
"// Mas de 5000 iconos disponibles — ver core/icons_tabler.h"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// toolbar
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_toolbar() {
|
||||
demo_header("toolbar", "v1.0.0",
|
||||
"Grupo horizontal con spacing consistente y separadores verticales sutiles. "
|
||||
"El caller usa ImGui::SameLine entre items y toolbar_separator entre grupos.");
|
||||
|
||||
section("Example with two groups");
|
||||
toolbar_begin();
|
||||
button(TI_PLUS " New", V::Primary); ImGui::SameLine();
|
||||
button(TI_FOLDER_OPEN " Open", V::Secondary); ImGui::SameLine();
|
||||
button(TI_DEVICE_FLOPPY " Save",V::Secondary);
|
||||
toolbar_separator();
|
||||
icon_button("##set", TI_SETTINGS, "Settings");
|
||||
ImGui::SameLine();
|
||||
icon_button("##help", TI_HELP, "Help");
|
||||
toolbar_end();
|
||||
|
||||
code_block(
|
||||
"#include \"core/icons_tabler.h\"\n\n"
|
||||
"toolbar_begin();\n"
|
||||
" button(TI_PLUS \" New\", V::Primary); ImGui::SameLine();\n"
|
||||
" button(TI_FOLDER_OPEN \" Open\", V::Secondary);\n"
|
||||
" toolbar_separator();\n"
|
||||
" icon_button(\"##set\", TI_SETTINGS, \"Settings\");\n"
|
||||
"toolbar_end();"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// modal_dialog
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_modal() {
|
||||
demo_header("modal_dialog", "v1.0.0",
|
||||
"Popup modal centrada con estilo surface+border. Close con Escape o click en X. "
|
||||
"Patron begin/end — modal_dialog_end debe llamarse siempre.");
|
||||
|
||||
static bool show = false;
|
||||
if (button("Open modal", V::Primary)) show = true;
|
||||
|
||||
if (modal_dialog_begin("Demo modal", &show, ImVec2(380, 0))) {
|
||||
ImGui::TextWrapped(
|
||||
"Modal centrada en el viewport principal, con estilo tokens.");
|
||||
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm));
|
||||
static char buf[64] = {};
|
||||
text_input("Name", buf, sizeof(buf), "escribe algo");
|
||||
ImGui::Separator();
|
||||
if (button("Cancel", V::Subtle)) show = false;
|
||||
ImGui::SameLine();
|
||||
if (button("Done", V::Primary)) show = false;
|
||||
}
|
||||
modal_dialog_end();
|
||||
|
||||
code_block(
|
||||
"static bool show = false;\n"
|
||||
"if (button(\"Open\", Primary)) show = true;\n"
|
||||
"if (modal_dialog_begin(\"Title\", &show, ImVec2(380,0))) {\n"
|
||||
" // ... campos del form ...\n"
|
||||
" if (button(\"Done\", Primary)) show = false;\n"
|
||||
"}\n"
|
||||
"modal_dialog_end();"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// text_input
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_text_input() {
|
||||
demo_header("text_input", "v1.0.0",
|
||||
"Label muted + input estilizado con tokens. Full-width dentro del contenedor. "
|
||||
"Placeholder opcional mostrado en text_dim cuando el buffer esta vacio.");
|
||||
|
||||
static char name[128] = {};
|
||||
static char desc[256] = {};
|
||||
static char tags[128] = {};
|
||||
|
||||
ImGui::BeginChild("##ti_wrap", ImVec2(420, 0), ImGuiChildFlags_AutoResizeY);
|
||||
text_input("Name", name, sizeof(name), "my-new-thing");
|
||||
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
|
||||
text_input("Description", desc, sizeof(desc));
|
||||
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
|
||||
text_input("Tags (CSV)", tags, sizeof(tags), "imgui,ui,form");
|
||||
ImGui::EndChild();
|
||||
|
||||
code_block(
|
||||
"static char name[128] = {};\n"
|
||||
"text_input(\"Name\", name, sizeof(name), \"my-new-thing\");\n"
|
||||
"// true on change — se usa mas para validar en vivo\n"
|
||||
"// que para leer el valor (que vive en el buffer)."
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// select
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_select() {
|
||||
demo_header("select", "v1.0.0",
|
||||
"Dropdown con label muted y opcion (none) opcional. Mismo estilo tokens que text_input.");
|
||||
|
||||
static int lang_idx = 0;
|
||||
static int domain_idx = -1;
|
||||
const char* langs[] = {"go", "py", "ts", "sh", "cpp"};
|
||||
const char* domains[] = {"core", "infra", "finance", "datascience", "viz"};
|
||||
|
||||
ImGui::BeginChild("##sl_wrap", ImVec2(420, 0), ImGuiChildFlags_AutoResizeY);
|
||||
select("Language", &lang_idx, langs, 5);
|
||||
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
|
||||
select("Domain (optional)", &domain_idx, domains, 5, true);
|
||||
ImGui::EndChild();
|
||||
|
||||
code_block(
|
||||
"static int lang = 0;\n"
|
||||
"const char* langs[] = {\"go\",\"py\",\"ts\",\"sh\",\"cpp\"};\n"
|
||||
"select(\"Language\", &lang, langs, 5);"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// toast + inbox
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_toast() {
|
||||
demo_header("toast", "v1.1.0",
|
||||
"Notificaciones efimeras (~3.5s con fade-out) + inbox con campana. "
|
||||
"La campana muestra badge con no-leidos y popover con las ultimas 50.");
|
||||
|
||||
section("Trigger toasts");
|
||||
if (button("Info", V::Secondary)) toast_push(ToastKind::Info, "this is an info toast");
|
||||
ImGui::SameLine();
|
||||
if (button("Success", V::Primary)) toast_push(ToastKind::Success, "operation completed");
|
||||
ImGui::SameLine();
|
||||
if (button("Warning", V::Secondary)) toast_push(ToastKind::Warning, "heads up about something");
|
||||
ImGui::SameLine();
|
||||
if (button("Error", V::Danger)) toast_push(ToastKind::Error, "operation failed: reason");
|
||||
|
||||
section("Inbox (bell with unread badge)");
|
||||
toast_inbox_button("##inbox_demo");
|
||||
|
||||
code_block(
|
||||
"toast_push(ToastKind::Success, \"Reindexed 891 functions\");\n"
|
||||
"toast_push(ToastKind::Error, \"HTTP 503: server down\");\n\n"
|
||||
"// En la toolbar:\n"
|
||||
"toast_inbox_button(\"##inbox\");\n\n"
|
||||
"// Una vez por frame al final del render:\n"
|
||||
"toast_render();"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// tree_view
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_tree_view() {
|
||||
demo_header("tree_view", "v1.0.0",
|
||||
"Tree low-level para jerarquias (ej. projects -> apps/analysis/vaults). "
|
||||
"Sin estado interno: el caller gestiona seleccion y pasa 'selected' por parametro.");
|
||||
|
||||
static std::string selected;
|
||||
|
||||
section("Projects (fake)");
|
||||
ImGui::BeginChild("##tv", ImVec2(360, 200), ImGuiChildFlags_Borders);
|
||||
|
||||
struct FakeProject { const char* id; const char* name; const char* apps[3]; };
|
||||
const FakeProject projs[] = {
|
||||
{"app_turismo", "app_turismo", {"guide_es", "offline_maps", nullptr}},
|
||||
{"element_agents", "element_agents", {"matrix_bot", nullptr, nullptr}},
|
||||
{"fn_monitoring", "fn_monitoring", {"sqlite_api", "registry_dashboard", nullptr}},
|
||||
};
|
||||
for (auto& p : projs) {
|
||||
bool sel = (selected == p.id);
|
||||
if (tree_branch_begin(p.id, p.name, sel)) {
|
||||
if (tree_node_clicked()) selected = p.id;
|
||||
for (int i = 0; i < 3 && p.apps[i]; i++) {
|
||||
bool asel = (selected == p.apps[i]);
|
||||
tree_leaf(p.apps[i], p.apps[i], asel);
|
||||
if (tree_node_clicked()) selected = p.apps[i];
|
||||
}
|
||||
tree_branch_end();
|
||||
} else if (tree_node_clicked()) {
|
||||
selected = p.id;
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
||||
ImGui::Text("Selected: %s", selected.empty() ? "(none)" : selected.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
code_block(
|
||||
"static std::string sel;\n"
|
||||
"if (tree_branch_begin(p.id, p.name, sel == p.id)) {\n"
|
||||
" if (tree_node_clicked()) sel = p.id;\n"
|
||||
" for (auto& a : p.apps) {\n"
|
||||
" tree_leaf(a.id, a.name, sel == a.id);\n"
|
||||
" if (tree_node_clicked()) sel = a.id;\n"
|
||||
" }\n"
|
||||
" tree_branch_end();\n"
|
||||
"}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// kpi_card
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_kpi_card() {
|
||||
demo_header("kpi_card", "v1.3.0",
|
||||
"Card compacta 86px con icono opcional + label muted, valor x1.4, trend con "
|
||||
"TI_TRENDING_UP/DOWN y sparkline. Usa tokens: surface bg, border, radius md.");
|
||||
|
||||
if (ImGui::BeginTable("##kpi_grid", 4, ImGuiTableFlags_SizingStretchSame)) {
|
||||
float history[] = {10, 12, 11, 15, 18, 17, 20};
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0); kpi_card("Revenue", 20000.0f, 12.5f, history, 7, "$%.0f", TI_CASH);
|
||||
ImGui::TableSetColumnIndex(1); kpi_card("Users", 1250.0f, 3.4f, history, 7, "%.0f", TI_USERS);
|
||||
ImGui::TableSetColumnIndex(2); kpi_card("Churn", 2.1f, -0.3f, history, 7, "%.1f%%", TI_CHART_BAR);
|
||||
ImGui::TableSetColumnIndex(3); kpi_card("Errors", 0.0f, 0.0f, nullptr, 0, "%.0f", TI_ALERT_CIRCLE);
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
code_block(
|
||||
"#include \"core/icons_tabler.h\"\n\n"
|
||||
"float history[] = {10,12,11,15,18,17,20};\n"
|
||||
"kpi_card(\"Revenue\", 20000.0f, 12.5f, history, 7, \"$%.0f\", TI_CASH);\n"
|
||||
"kpi_card(\"Users\", 1250.0f, 3.4f, history, 7, \"%.0f\", TI_USERS);\n"
|
||||
"// Sin delta ni history: muestra TI_MINUS como placeholder\n"
|
||||
"kpi_card(\"Errors\", 0.0f, 0.0f, nullptr, 0, \"%.0f\", TI_ALERT_CIRCLE);"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// badge
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_badge() {
|
||||
demo_header("badge", "v1.0.0",
|
||||
"Etiqueta inline con 6 variantes semanticas. Equivalente a <Badge> de fn_library.");
|
||||
|
||||
section("Variants");
|
||||
badge("Default", BadgeVariant::Default); ImGui::SameLine();
|
||||
badge("Success", BadgeVariant::Success); ImGui::SameLine();
|
||||
badge("Warning", BadgeVariant::Warning); ImGui::SameLine();
|
||||
badge("Error", BadgeVariant::Error); ImGui::SameLine();
|
||||
badge("Info", BadgeVariant::Info); ImGui::SameLine();
|
||||
badge("Outline", BadgeVariant::Outline);
|
||||
|
||||
section("In context (table row)");
|
||||
ImGui::Text("filter_slice_go_core"); ImGui::SameLine();
|
||||
badge("pure", BadgeVariant::Success); ImGui::SameLine();
|
||||
badge("tested", BadgeVariant::Info);
|
||||
|
||||
code_block(
|
||||
"badge(\"pure\", BadgeVariant::Success);\n"
|
||||
"badge(\"stale\", BadgeVariant::Warning);\n"
|
||||
"badge(\"broken\", BadgeVariant::Error);"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// empty_state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_empty_state() {
|
||||
demo_header("empty_state", "v1.0.0",
|
||||
"Icono grande muted + titulo + descripcion opcional. Para listas/tablas vacias.");
|
||||
|
||||
ImGui::BeginChild("##es", ImVec2(0, 180), ImGuiChildFlags_Borders);
|
||||
empty_state("( no data )", "No projects yet",
|
||||
"Create one under projects/{name}/ with project.md and reindex");
|
||||
ImGui::EndChild();
|
||||
|
||||
code_block(
|
||||
"if (apps.empty()) {\n"
|
||||
" empty_state(\"( no data )\", \"No apps yet\",\n"
|
||||
" \"Click + Add to create one\");\n"
|
||||
" return;\n"
|
||||
"}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// page_header
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_page_header() {
|
||||
demo_header("page_header", "v1.0.0",
|
||||
"Header de pagina con titulo, subtitulo opcional y separador final. "
|
||||
"Patron begin/end permite insertar acciones entre titulo y separador.");
|
||||
|
||||
page_header_begin("Dashboard", "13 apps, 3 projects, 2 analyses");
|
||||
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 140.0f);
|
||||
toolbar_begin();
|
||||
button("Reload", V::Subtle); ImGui::SameLine();
|
||||
button("+ Add", V::Secondary);
|
||||
toolbar_end();
|
||||
page_header_end();
|
||||
|
||||
code_block(
|
||||
"page_header_begin(\"Dashboard\", subtitle);\n"
|
||||
"ImGui::SameLine(ImGui::GetContentRegionAvail().x - 140);\n"
|
||||
"toolbar_begin();\n"
|
||||
" button(\"Reload\", Subtle);\n"
|
||||
"toolbar_end();\n"
|
||||
"page_header_end();"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// dashboard_panel
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_dashboard_panel() {
|
||||
demo_header("dashboard_panel", "v1.0.0",
|
||||
"Contenedor tipo panel con titulo, bordes redondeados, bg surface. "
|
||||
"Auto-resize-Y segun contenido. Usa min_width/min_height como piso.");
|
||||
|
||||
if (dashboard_panel_begin("Revenue", 0, 120.0f)) {
|
||||
ImGui::Text("Some panel content goes here.");
|
||||
ImGui::Text("Anything drawn inside lives in the child window.");
|
||||
}
|
||||
dashboard_panel_end();
|
||||
|
||||
code_block(
|
||||
"if (dashboard_panel_begin(\"Revenue\", 0, 120.0f)) {\n"
|
||||
" ImGui::Text(\"content\");\n"
|
||||
"}\n"
|
||||
"dashboard_panel_end();"
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
@@ -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 <imgui.h>
|
||||
#include <chrono>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
namespace {
|
||||
|
||||
// Fragment shader sintetico — gradiente animado con celdas. Usa los uniforms
|
||||
// estandar que compile_fragment inyecta: u_resolution, u_time, u_mouse.
|
||||
const char* kShaderSrc = R"(
|
||||
void mainImage() {
|
||||
vec2 uv = gl_FragCoord.xy / u_resolution;
|
||||
vec2 cell = uv * 8.0;
|
||||
vec2 ipos = floor(cell);
|
||||
vec2 fpos = fract(cell) - 0.5;
|
||||
|
||||
float t = u_time * 0.6;
|
||||
float wave = sin(ipos.x * 0.7 + ipos.y * 0.5 + t);
|
||||
float dist = length(fpos);
|
||||
|
||||
vec3 a = vec3(0.30, 0.43, 0.96); // indigo
|
||||
vec3 b = vec3(0.95, 0.45, 0.85); // pink
|
||||
vec3 col = mix(a, b, 0.5 + 0.5 * wave);
|
||||
|
||||
// Mouse focus: oscurecemos celdas lejanas al cursor.
|
||||
vec2 m = u_mouse / u_resolution;
|
||||
float fm = 1.0 - smoothstep(0.0, 0.6, length(uv - m));
|
||||
col *= 0.6 + 0.4 * fm;
|
||||
|
||||
// Disco interior por celda con borde suave.
|
||||
col *= smoothstep(0.5, 0.45, dist);
|
||||
|
||||
fragColor = vec4(col, 1.0);
|
||||
}
|
||||
|
||||
void main() {
|
||||
mainImage();
|
||||
}
|
||||
)";
|
||||
|
||||
struct CanvasState {
|
||||
fn::gfx::ShaderCanvas canvas;
|
||||
bool compiled = false;
|
||||
bool compile_failed = false;
|
||||
std::string err_msg;
|
||||
std::chrono::steady_clock::time_point t0;
|
||||
};
|
||||
|
||||
CanvasState& state() {
|
||||
static CanvasState s;
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void demo_shader_canvas() {
|
||||
demo_header("shader_canvas", "v1.0.0",
|
||||
"Framebuffer + fullscreen quad + shader GLSL animado. La misma pieza "
|
||||
"que usa shaders_lab para el preview en vivo. Uniforms u_time / u_resolution / u_mouse "
|
||||
"los inyecta gl_shader::compile_fragment automaticamente.");
|
||||
|
||||
auto& s = state();
|
||||
|
||||
// Compilacion lazy (en el primer frame ya hay contexto GL valido).
|
||||
if (!s.compiled && !s.compile_failed) {
|
||||
fn::gfx::gl_loader_init();
|
||||
fn::gfx::canvas_init(s.canvas);
|
||||
|
||||
auto cr = fn::gfx::compile_fragment(kShaderSrc);
|
||||
if (!cr.ok) {
|
||||
s.compile_failed = true;
|
||||
s.err_msg = cr.err_msg;
|
||||
} else {
|
||||
fn::gfx::canvas_set_program(s.canvas, cr.program);
|
||||
s.t0 = std::chrono::steady_clock::now();
|
||||
s.compiled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (s.compile_failed) {
|
||||
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1),
|
||||
"Compilacion del fragment shader fallo:\n%s",
|
||||
s.err_msg.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
section("Live preview");
|
||||
|
||||
// Render del shader en un panel ~480x300 px. canvas_render hace resize
|
||||
// automatico segun GetContentRegionAvail si lo dejas crecer.
|
||||
ImGui::BeginChild("##shader_preview", ImVec2(480, 300),
|
||||
ImGuiChildFlags_Borders);
|
||||
const float dt = std::chrono::duration<float>(
|
||||
std::chrono::steady_clock::now() - s.t0).count();
|
||||
fn::gfx::canvas_render(s.canvas, dt);
|
||||
ImGui::EndChild();
|
||||
|
||||
code_block(
|
||||
"#include \"gfx/shader_canvas.h\"\n"
|
||||
"#include \"gfx/gl_shader.h\"\n\n"
|
||||
"static fn::gfx::ShaderCanvas canvas;\n"
|
||||
"// Setup (una vez):\n"
|
||||
"fn::gfx::canvas_init(canvas);\n"
|
||||
"auto cr = fn::gfx::compile_fragment(user_glsl);\n"
|
||||
"if (cr.ok) fn::gfx::canvas_set_program(canvas, cr.program);\n\n"
|
||||
"// Cada frame, dentro de un Begin/End:\n"
|
||||
"fn::gfx::canvas_render(canvas, time_seconds);"
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
@@ -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 <imgui.h>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
|
||||
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<GraphNode>& nodes_out,
|
||||
std::vector<GraphEdge>& 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<float>((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<float> cluster_cx(K), cluster_cy(K);
|
||||
for (int k = 0; k < K; k++) {
|
||||
float angle = 2.0f * 3.14159f * k / K;
|
||||
cluster_cx[k] = std::cos(angle) * 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<uint32_t>(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<uint32_t>(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<int>(rnd() * size);
|
||||
add_edge(static_cast<uint32_t>(i),
|
||||
static_cast<uint32_t>(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<uint32_t>(rnd() * N);
|
||||
uint32_t b = static_cast<uint32_t>(rnd() * N);
|
||||
add_edge(a, b, 0.3f);
|
||||
}
|
||||
}
|
||||
|
||||
void demo_graph() {
|
||||
demo_header("graph_viewport", "v1.0.0",
|
||||
"Pipeline completo de visualizacion de grafos: graph_renderer (instanced GPU) "
|
||||
"+ graph_force_layout (Barnes-Hut) + graph_spatial_hash (hit-testing). "
|
||||
"Render a FBO mostrado via ImGui::Image — escala a decenas de miles de nodos.");
|
||||
|
||||
static int s_n_nodes = 1000;
|
||||
static int s_n_clusters = 6;
|
||||
static float s_repulsion = 3500.0f; // fuerza de dispersion entre nodos
|
||||
static float s_attraction = 0.02f; // muelle entre nodos conectados
|
||||
static float s_gravity = 0.001f; // tiron hacia el centro
|
||||
static std::vector<GraphNode> s_nodes;
|
||||
static std::vector<GraphEdge> s_edges;
|
||||
static GraphData s_graph{};
|
||||
static GraphViewportState s_state;
|
||||
static bool s_initialized = false;
|
||||
static bool s_needs_regen = true;
|
||||
|
||||
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<int>(s_nodes.size());
|
||||
s_graph.edges = s_edges.data();
|
||||
s_graph.edge_count = static_cast<int>(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
|
||||
@@ -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 <imgui.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
|
||||
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<EventLogEntry> 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
|
||||
@@ -0,0 +1,211 @@
|
||||
#include "demos.h"
|
||||
#include "demo.h"
|
||||
|
||||
#include "viz/bar_chart.h"
|
||||
#include "viz/pie_chart.h"
|
||||
#include "viz/line_plot.h"
|
||||
#include "viz/scatter_plot.h"
|
||||
#include "viz/histogram.h"
|
||||
#include "viz/sparkline.h"
|
||||
#include "core/tokens.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// bar_chart
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_bar_chart() {
|
||||
demo_header("bar_chart", "v1.2.0",
|
||||
"Barras verticales con ejes pineados, tooltip al hover y auto-rotacion 45 grados "
|
||||
"de labels cuando no caben horizontalmente.");
|
||||
|
||||
section("Labels que caben horizontalmente");
|
||||
{
|
||||
const char* langs[] = {"go", "py", "ts", "sh", "cpp"};
|
||||
float values[] = {412.0f, 187.0f, 94.0f, 63.0f, 36.0f};
|
||||
bar_chart("##bar_short", langs, values, 5, 0.67f, 200.0f);
|
||||
}
|
||||
|
||||
section("Labels largos que obligan a rotar");
|
||||
{
|
||||
const char* domains[] = {
|
||||
"core", "infrastructure", "finance", "datascience",
|
||||
"cybersecurity", "notebook", "browser"
|
||||
};
|
||||
float values[] = {412, 187, 94, 63, 42, 38, 22};
|
||||
bar_chart("##bar_long", domains, values, 7, 0.67f, 240.0f);
|
||||
}
|
||||
|
||||
code_block(
|
||||
"const char* labels[] = {\"go\",\"py\",\"ts\",\"sh\",\"cpp\"};\n"
|
||||
"float values[] = {412,187,94,63,36};\n"
|
||||
"bar_chart(\"##lang\", labels, values, 5); // h=200 default\n"
|
||||
"bar_chart(\"##lang\", labels, values, 5, 0.8f, 300); // bar_w + altura"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// pie_chart
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_pie_chart() {
|
||||
demo_header("pie_chart", "v1.1.0",
|
||||
"Pie/donut con aspect 1:1, ejes pineados y tooltip por slice con "
|
||||
"valor absoluto + porcentaje.");
|
||||
|
||||
if (ImGui::BeginTable("##pie_grid", 2, ImGuiTableFlags_SizingStretchSame)) {
|
||||
ImGui::TableNextRow();
|
||||
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
{
|
||||
const char* labels[] = {"Pure", "Impure"};
|
||||
float values[] = {412.0f, 278.0f};
|
||||
variant_label("Pie (radius auto)");
|
||||
pie_chart("##pie_auto", labels, values, 2, 0.0f, 260.0f);
|
||||
}
|
||||
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
{
|
||||
const char* labels[] = {"function", "pipeline", "component"};
|
||||
float values[] = {618.0f, 42.0f, 230.0f};
|
||||
variant_label("Donut (radius = -0.45)");
|
||||
pie_chart("##pie_donut", labels, values, 3, -0.45f, 260.0f);
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
code_block(
|
||||
"const char* labels[] = {\"Pure\",\"Impure\"};\n"
|
||||
"float values[] = {412, 278};\n"
|
||||
"pie_chart(\"##p\", labels, values, 2); // pie auto\n"
|
||||
"pie_chart(\"##p\", labels, values, 2, -0.45f, 260); // donut"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// line_plot
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_line_plot() {
|
||||
demo_header("line_plot", "v1.1.0",
|
||||
"Line plot 2D con limites de ejes calculados de min/max y pineados. "
|
||||
"Sin auto-fit animado, sin pan/zoom.");
|
||||
|
||||
constexpr int N = 100;
|
||||
static float xs[N], ys[N];
|
||||
static bool init = false;
|
||||
if (!init) {
|
||||
for (int i = 0; i < N; i++) {
|
||||
xs[i] = static_cast<float>(i) * 0.1f;
|
||||
ys[i] = std::sin(xs[i]) + 0.3f * std::sin(xs[i] * 3.5f);
|
||||
}
|
||||
init = true;
|
||||
}
|
||||
line_plot("##line", xs, ys, N, 240.0f);
|
||||
|
||||
code_block(
|
||||
"line_plot(\"##series\", xs, ys, count); // h=200 default\n"
|
||||
"line_plot(\"##series\", xs, ys, count, 300.0f); // custom height"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// scatter_plot
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_scatter_plot() {
|
||||
demo_header("scatter_plot", "v1.1.0",
|
||||
"Puntos dispersos con ejes pineados (5% headroom). Sin interaccion.");
|
||||
|
||||
constexpr int N = 120;
|
||||
static float xs[N], ys[N];
|
||||
static bool init = false;
|
||||
if (!init) {
|
||||
unsigned seed = 1234;
|
||||
auto rnd = [&]() {
|
||||
seed = seed * 1103515245u + 12345u;
|
||||
return static_cast<float>((seed >> 16) & 0x7fff) / 32768.0f;
|
||||
};
|
||||
for (int i = 0; i < N; i++) {
|
||||
xs[i] = rnd() * 10.0f;
|
||||
ys[i] = 0.5f * xs[i] + rnd() * 3.0f;
|
||||
}
|
||||
init = true;
|
||||
}
|
||||
scatter_plot("##sc", xs, ys, N, 240.0f);
|
||||
|
||||
code_block(
|
||||
"scatter_plot(\"##xy\", xs, ys, count, 240.0f);"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// histogram
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_histogram() {
|
||||
demo_header("histogram", "v1.1.0",
|
||||
"Histograma con bins automaticos (Sturges) o manuales. Usa AutoFit "
|
||||
"para los bins + Lock para bloquear pan/zoom.");
|
||||
|
||||
constexpr int N = 300;
|
||||
static float vals[N];
|
||||
static bool init = false;
|
||||
if (!init) {
|
||||
unsigned seed = 42;
|
||||
auto rnd = [&]() {
|
||||
seed = seed * 1103515245u + 12345u;
|
||||
return static_cast<float>((seed >> 16) & 0x7fff) / 32768.0f;
|
||||
};
|
||||
// Aproximacion de distribucion normal via box-muller simplificado
|
||||
for (int i = 0; i < N; i++) {
|
||||
float u1 = rnd() + 1e-6f;
|
||||
float u2 = rnd();
|
||||
vals[i] = std::sqrt(-2.0f * std::log(u1))
|
||||
* std::cos(2.0f * 3.14159f * u2);
|
||||
}
|
||||
init = true;
|
||||
}
|
||||
histogram("##hist", vals, N, -1, 240.0f);
|
||||
|
||||
code_block(
|
||||
"histogram(\"##h\", values, count); // bins=Sturges\n"
|
||||
"histogram(\"##h\", values, count, 30, 300.0f); // 30 bins, h=300"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// sparkline
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_sparkline() {
|
||||
demo_header("sparkline", "v1.0.0",
|
||||
"Mini grafico de lineas inline (rellenado con alpha + linea). "
|
||||
"Pensado para tablas, KPI cards, headers.");
|
||||
|
||||
float up[] = {10, 12, 11, 15, 18, 17, 20};
|
||||
float down[] = {30, 28, 29, 25, 22, 24, 20};
|
||||
float flat[] = {10, 10, 10, 10, 10, 10, 10};
|
||||
|
||||
ImGui::Text("Trending up "); ImGui::SameLine();
|
||||
sparkline("##up", up, 7, ImVec4(0.35f, 0.85f, 0.45f, 1.0f), 140.0f, 22.0f);
|
||||
|
||||
ImGui::Text("Trending down"); ImGui::SameLine();
|
||||
sparkline("##down", down, 7, ImVec4(0.90f, 0.30f, 0.30f, 1.0f), 140.0f, 22.0f);
|
||||
|
||||
ImGui::Text("Flat "); ImGui::SameLine();
|
||||
sparkline("##flat", flat, 7, ImVec4(0.55f, 0.55f, 0.55f, 1.0f), 140.0f, 22.0f);
|
||||
|
||||
code_block(
|
||||
"float history[] = {10,12,11,15,18,17,20};\n"
|
||||
"sparkline(\"##rev\", history, 7, /*color=*/{0.35,0.85,0.45,1}, 140, 22);"
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
@@ -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 <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct DemoEntry {
|
||||
const char* id; // id estable, apto para comparar seleccion
|
||||
const char* label; // texto en sidebar
|
||||
const char* category; // "Core" o "Viz"
|
||||
void (*fn)(); // puntero a la demo_xxx
|
||||
};
|
||||
|
||||
static const DemoEntry k_demos[] = {
|
||||
// Core
|
||||
{"button", "button", "Core", &gallery::demo_button},
|
||||
{"icon_button", "icon_button", "Core", &gallery::demo_icon_button},
|
||||
{"toolbar", "toolbar", "Core", &gallery::demo_toolbar},
|
||||
{"modal_dialog", "modal_dialog", "Core", &gallery::demo_modal},
|
||||
{"text_input", "text_input", "Core", &gallery::demo_text_input},
|
||||
{"select", "select", "Core", &gallery::demo_select},
|
||||
{"toast", "toast + inbox", "Core", &gallery::demo_toast},
|
||||
{"tree_view", "tree_view", "Core", &gallery::demo_tree_view},
|
||||
{"badge", "badge", "Core", &gallery::demo_badge},
|
||||
{"empty_state", "empty_state", "Core", &gallery::demo_empty_state},
|
||||
{"page_header", "page_header", "Core", &gallery::demo_page_header},
|
||||
{"dashboard_panel", "dashboard_panel", "Core", &gallery::demo_dashboard_panel},
|
||||
{"kpi_card", "kpi_card", "Core", &gallery::demo_kpi_card},
|
||||
{"text_editor", "text_editor + 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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user