fix(fn-run): propagar stdout/stderr de bash functions library-style #1
@@ -115,6 +115,19 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/shaders_lab/CMakeLists.txt)
|
||||
add_subdirectory(apps/shaders_lab)
|
||||
endif()
|
||||
|
||||
# --- Primitives Gallery ---
|
||||
# Activado solo si la app esta presente Y todos sus deps tambien (button, toolbar...
|
||||
# son sources untracked en este worktree). Forzar con FN_BUILD_GALLERY=ON.
|
||||
if(FN_BUILD_GALLERY AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/primitives_gallery/CMakeLists.txt)
|
||||
add_subdirectory(apps/primitives_gallery)
|
||||
endif()
|
||||
|
||||
# --- text_editor + file_watcher smoke test (issue 0025) ---
|
||||
# Build gate para validar que text_editor.cpp + file_watcher.cpp + vendor enlazan.
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/text_editor_smoke/CMakeLists.txt)
|
||||
add_subdirectory(apps/text_editor_smoke)
|
||||
endif()
|
||||
|
||||
# --- Registry Dashboard (lives in projects/fn_monitoring/apps/) ---
|
||||
set(_DASH_DIR ${CMAKE_SOURCE_DIR}/../projects/fn_monitoring/apps/registry_dashboard)
|
||||
if(EXISTS ${_DASH_DIR}/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()
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# Smoke test app para validar que text_editor + file_watcher compilan
|
||||
# y enlazan correctamente. NO es una app del registry, solo build gate
|
||||
# de las funciones nuevas del issue 0025. Sin ImGui events runtime — el
|
||||
# test crea, settea texto, polea y destruye en 1 frame headless (no abre ventana).
|
||||
|
||||
add_imgui_app(text_editor_smoke
|
||||
main.cpp
|
||||
${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
|
||||
)
|
||||
target_include_directories(text_editor_smoke PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit
|
||||
)
|
||||
@@ -0,0 +1,64 @@
|
||||
// Smoke test (no GUI): compila y ejecuta brevemente las APIs nuevas del
|
||||
// issue 0025 para validar que el wrapper PIMPL del text_editor y el
|
||||
// file_watcher (inotify Linux / ReadDirectoryChangesW Win) enlazan.
|
||||
//
|
||||
// No abre ventana ImGui — solo crea / settea texto / lee / poll / destruye.
|
||||
|
||||
#include "core/text_editor.h"
|
||||
#include "core/file_watcher.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
int main() {
|
||||
// ----- text_editor -----
|
||||
auto* ed = fn::text_editor_create(fn::CodeLang::GLSL);
|
||||
if (!ed) { std::fprintf(stderr, "text_editor_create returned null\n"); return 1; }
|
||||
|
||||
fn::text_editor_set_text(ed, "void main(){}\n");
|
||||
const char* got = fn::text_editor_get_text(ed);
|
||||
std::printf("text_editor: get_text -> %zu bytes\n", got ? std::strlen(got) : 0u);
|
||||
|
||||
if (fn::text_editor_is_dirty(ed)) {
|
||||
std::fprintf(stderr, "text_editor: dirty unexpected after set_text\n");
|
||||
return 1;
|
||||
}
|
||||
fn::text_editor_destroy(ed);
|
||||
|
||||
// ----- file_watcher -----
|
||||
const char* path = "/tmp/fn_smoke_test.txt";
|
||||
std::remove(path);
|
||||
{
|
||||
FILE* f = std::fopen(path, "w"); std::fputs("init\n", f); std::fclose(f);
|
||||
}
|
||||
|
||||
auto* fw = fn::file_watcher_create();
|
||||
if (!fw) { std::fprintf(stderr, "file_watcher_create returned null\n"); return 1; }
|
||||
|
||||
if (!fn::file_watcher_add(fw, path)) {
|
||||
std::fprintf(stderr, "file_watcher_add failed: %s\n", fn::file_watcher_last_error(fw));
|
||||
// Aun asi continuamos: en CI sin inotify (raro) este test seria flaky.
|
||||
}
|
||||
|
||||
// Modificar
|
||||
{
|
||||
FILE* f = std::fopen(path, "w"); std::fputs("changed\n", f); std::fclose(f);
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
auto evs = fn::file_watcher_poll(fw);
|
||||
std::printf("file_watcher: %zu events\n", evs.size());
|
||||
for (auto& e : evs) {
|
||||
const char* kind = e.kind == fn::FileEvent::Modified ? "MOD"
|
||||
: e.kind == fn::FileEvent::Created ? "NEW" : "DEL";
|
||||
std::printf(" [%s] %s\n", kind, e.path.c_str());
|
||||
}
|
||||
|
||||
fn::file_watcher_destroy(fw);
|
||||
std::remove(path);
|
||||
std::printf("OK\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
#include "file_watcher.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <sys/inotify.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <poll.h>
|
||||
#elif defined(_WIN32)
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
namespace fn {
|
||||
|
||||
#if defined(__linux__)
|
||||
|
||||
struct FileWatcher {
|
||||
int fd = -1;
|
||||
std::unordered_map<int, std::string> wd_to_path; // inotify wd -> path
|
||||
std::string last_err;
|
||||
};
|
||||
|
||||
FileWatcher* file_watcher_create() {
|
||||
auto* w = new FileWatcher();
|
||||
w->fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
|
||||
if (w->fd < 0) {
|
||||
w->last_err = std::string("inotify_init1: ") + std::strerror(errno);
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
void file_watcher_destroy(FileWatcher* w) {
|
||||
if (!w) return;
|
||||
if (w->fd >= 0) ::close(w->fd);
|
||||
delete w;
|
||||
}
|
||||
|
||||
bool file_watcher_add(FileWatcher* w, const char* path) {
|
||||
if (!w || w->fd < 0 || !path) return false;
|
||||
const uint32_t mask = IN_MODIFY | IN_CREATE | IN_DELETE
|
||||
| IN_CLOSE_WRITE | IN_MOVED_FROM | IN_MOVED_TO
|
||||
| IN_DELETE_SELF | IN_MOVE_SELF;
|
||||
int wd = inotify_add_watch(w->fd, path, mask);
|
||||
if (wd < 0) {
|
||||
w->last_err = std::string("inotify_add_watch(") + path + "): " + std::strerror(errno);
|
||||
return false;
|
||||
}
|
||||
w->wd_to_path[wd] = path;
|
||||
w->last_err.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<FileEvent> file_watcher_poll(FileWatcher* w) {
|
||||
std::vector<FileEvent> out;
|
||||
if (!w || w->fd < 0) return out;
|
||||
|
||||
// Drain todos los eventos pendientes sin bloquear.
|
||||
char buf[4096] __attribute__((aligned(__alignof__(struct inotify_event))));
|
||||
while (true) {
|
||||
ssize_t n = ::read(w->fd, buf, sizeof(buf));
|
||||
if (n <= 0) {
|
||||
if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
w->last_err = std::string("inotify read: ") + std::strerror(errno);
|
||||
}
|
||||
break;
|
||||
}
|
||||
for (char* p = buf; p < buf + n; ) {
|
||||
auto* ev = reinterpret_cast<struct inotify_event*>(p);
|
||||
FileEvent fe;
|
||||
auto it = w->wd_to_path.find(ev->wd);
|
||||
std::string base = (it != w->wd_to_path.end()) ? it->second : std::string();
|
||||
if (ev->len > 0) {
|
||||
if (!base.empty() && base.back() != '/') base += '/';
|
||||
base += ev->name;
|
||||
}
|
||||
fe.path = base;
|
||||
|
||||
if (ev->mask & (IN_CREATE | IN_MOVED_TO)) {
|
||||
fe.kind = FileEvent::Created;
|
||||
out.push_back(fe);
|
||||
}
|
||||
if (ev->mask & (IN_DELETE | IN_DELETE_SELF | IN_MOVED_FROM | IN_MOVE_SELF)) {
|
||||
fe.kind = FileEvent::Deleted;
|
||||
out.push_back(fe);
|
||||
}
|
||||
if (ev->mask & (IN_MODIFY | IN_CLOSE_WRITE)) {
|
||||
fe.kind = FileEvent::Modified;
|
||||
out.push_back(fe);
|
||||
}
|
||||
p += sizeof(struct inotify_event) + ev->len;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const char* file_watcher_last_error(const FileWatcher* w) {
|
||||
return w ? w->last_err.c_str() : "";
|
||||
}
|
||||
|
||||
#elif defined(_WIN32)
|
||||
|
||||
// Una entry por directorio vigilado: un OVERLAPPED + buffer + handle.
|
||||
// Para "vigilar un archivo" en Windows registramos su directorio padre y
|
||||
// filtramos por nombre en el poll().
|
||||
struct WinWatch {
|
||||
HANDLE dir = INVALID_HANDLE_VALUE;
|
||||
OVERLAPPED ovl = {};
|
||||
std::vector<uint8_t> buf;
|
||||
std::string dir_path; // directorio absoluto vigilado
|
||||
std::string filter_name; // si !empty: solo emitir eventos cuyo path == dir_path/filter_name
|
||||
bool pending = false;
|
||||
};
|
||||
|
||||
struct FileWatcher {
|
||||
std::vector<WinWatch*> watches;
|
||||
std::string last_err;
|
||||
};
|
||||
|
||||
static void start_read(WinWatch* ww) {
|
||||
if (ww->dir == INVALID_HANDLE_VALUE) return;
|
||||
DWORD bytes = 0;
|
||||
BOOL ok = ::ReadDirectoryChangesW(
|
||||
ww->dir,
|
||||
ww->buf.data(),
|
||||
(DWORD)ww->buf.size(),
|
||||
FALSE, // bWatchSubtree
|
||||
FILE_NOTIFY_CHANGE_FILE_NAME |
|
||||
FILE_NOTIFY_CHANGE_DIR_NAME |
|
||||
FILE_NOTIFY_CHANGE_LAST_WRITE|
|
||||
FILE_NOTIFY_CHANGE_SIZE |
|
||||
FILE_NOTIFY_CHANGE_CREATION,
|
||||
&bytes,
|
||||
&ww->ovl,
|
||||
NULL);
|
||||
ww->pending = (ok != 0);
|
||||
}
|
||||
|
||||
FileWatcher* file_watcher_create() {
|
||||
return new FileWatcher();
|
||||
}
|
||||
|
||||
void file_watcher_destroy(FileWatcher* w) {
|
||||
if (!w) return;
|
||||
for (auto* ww : w->watches) {
|
||||
if (ww->dir != INVALID_HANDLE_VALUE) ::CloseHandle(ww->dir);
|
||||
if (ww->ovl.hEvent) ::CloseHandle(ww->ovl.hEvent);
|
||||
delete ww;
|
||||
}
|
||||
delete w;
|
||||
}
|
||||
|
||||
static std::string dirname_of(const std::string& path) {
|
||||
size_t pos = path.find_last_of("\\/");
|
||||
if (pos == std::string::npos) return ".";
|
||||
return path.substr(0, pos);
|
||||
}
|
||||
static std::string basename_of(const std::string& path) {
|
||||
size_t pos = path.find_last_of("\\/");
|
||||
if (pos == std::string::npos) return path;
|
||||
return path.substr(pos + 1);
|
||||
}
|
||||
|
||||
bool file_watcher_add(FileWatcher* w, const char* path) {
|
||||
if (!w || !path) return false;
|
||||
DWORD attrs = ::GetFileAttributesA(path);
|
||||
if (attrs == INVALID_FILE_ATTRIBUTES) {
|
||||
w->last_err = std::string("GetFileAttributes(") + path + "): not found";
|
||||
return false;
|
||||
}
|
||||
std::string dir, filter;
|
||||
if (attrs & FILE_ATTRIBUTE_DIRECTORY) {
|
||||
dir = path;
|
||||
} else {
|
||||
dir = dirname_of(path);
|
||||
filter = basename_of(path);
|
||||
}
|
||||
HANDLE h = ::CreateFileA(
|
||||
dir.c_str(),
|
||||
FILE_LIST_DIRECTORY,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
NULL,
|
||||
OPEN_EXISTING,
|
||||
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
|
||||
NULL);
|
||||
if (h == INVALID_HANDLE_VALUE) {
|
||||
w->last_err = std::string("CreateFileA(") + dir + "): " + std::to_string(::GetLastError());
|
||||
return false;
|
||||
}
|
||||
auto* ww = new WinWatch();
|
||||
ww->dir = h;
|
||||
ww->buf.resize(8192);
|
||||
ww->ovl.hEvent = ::CreateEventA(NULL, TRUE, FALSE, NULL);
|
||||
ww->dir_path = dir;
|
||||
ww->filter_name = filter;
|
||||
start_read(ww);
|
||||
w->watches.push_back(ww);
|
||||
w->last_err.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::string narrow_w(const wchar_t* wstr, size_t wlen) {
|
||||
if (wlen == 0) return {};
|
||||
int n = ::WideCharToMultiByte(CP_UTF8, 0, wstr, (int)wlen, NULL, 0, NULL, NULL);
|
||||
std::string out(n, 0);
|
||||
::WideCharToMultiByte(CP_UTF8, 0, wstr, (int)wlen, out.data(), n, NULL, NULL);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<FileEvent> file_watcher_poll(FileWatcher* w) {
|
||||
std::vector<FileEvent> out;
|
||||
if (!w) return out;
|
||||
for (auto* ww : w->watches) {
|
||||
if (!ww->pending) { start_read(ww); continue; }
|
||||
DWORD bytes = 0;
|
||||
BOOL ok = ::GetOverlappedResult(ww->dir, &ww->ovl, &bytes, FALSE);
|
||||
if (!ok) {
|
||||
// ERROR_IO_INCOMPLETE => sin novedades; ignorar
|
||||
DWORD err = ::GetLastError();
|
||||
if (err != ERROR_IO_INCOMPLETE) {
|
||||
w->last_err = std::string("GetOverlappedResult: ") + std::to_string(err);
|
||||
ww->pending = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
ww->pending = false;
|
||||
::ResetEvent(ww->ovl.hEvent);
|
||||
|
||||
const uint8_t* p = ww->buf.data();
|
||||
const uint8_t* end = p + bytes;
|
||||
while (p < end) {
|
||||
auto* fni = reinterpret_cast<const FILE_NOTIFY_INFORMATION*>(p);
|
||||
std::string name = narrow_w(fni->FileName, fni->FileNameLength / sizeof(wchar_t));
|
||||
std::string full = ww->dir_path + "\\" + name;
|
||||
|
||||
bool match = ww->filter_name.empty() || ww->filter_name == name;
|
||||
if (match) {
|
||||
FileEvent fe;
|
||||
fe.path = full;
|
||||
switch (fni->Action) {
|
||||
case FILE_ACTION_ADDED:
|
||||
case FILE_ACTION_RENAMED_NEW_NAME:
|
||||
fe.kind = FileEvent::Created; out.push_back(fe); break;
|
||||
case FILE_ACTION_REMOVED:
|
||||
case FILE_ACTION_RENAMED_OLD_NAME:
|
||||
fe.kind = FileEvent::Deleted; out.push_back(fe); break;
|
||||
case FILE_ACTION_MODIFIED:
|
||||
fe.kind = FileEvent::Modified; out.push_back(fe); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
if (fni->NextEntryOffset == 0) break;
|
||||
p += fni->NextEntryOffset;
|
||||
}
|
||||
start_read(ww);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const char* file_watcher_last_error(const FileWatcher* w) {
|
||||
return w ? w->last_err.c_str() : "";
|
||||
}
|
||||
|
||||
#else // Other platforms — stub
|
||||
|
||||
struct FileWatcher { std::string last_err = "file_watcher: platform not supported"; };
|
||||
|
||||
FileWatcher* file_watcher_create() { return new FileWatcher(); }
|
||||
void file_watcher_destroy(FileWatcher* w) { delete w; }
|
||||
bool file_watcher_add(FileWatcher*, const char*) { return false; }
|
||||
std::vector<FileEvent> file_watcher_poll(FileWatcher*) { return {}; }
|
||||
const char* file_watcher_last_error(const FileWatcher* w) { return w ? w->last_err.c_str() : ""; }
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace fn
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
// file_watcher — watcher cross-platform de archivos/directorios (impure I/O).
|
||||
//
|
||||
// Linux: inotify (mascara IN_MODIFY | IN_CREATE | IN_DELETE | IN_CLOSE_WRITE | IN_MOVED_*)
|
||||
// Windows: ReadDirectoryChangesW (overlapped, no bloqueante en poll())
|
||||
// Otros: stub (poll() devuelve siempre vacio)
|
||||
//
|
||||
// API no bloqueante: poll() drena los eventos disponibles desde la ultima llamada.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fn {
|
||||
|
||||
struct FileWatcher; // PIMPL
|
||||
|
||||
struct FileEvent {
|
||||
enum Kind { Modified, Created, Deleted };
|
||||
std::string path;
|
||||
Kind kind;
|
||||
};
|
||||
|
||||
// Crea un watcher vacio. El caller llama destroy.
|
||||
FileWatcher* file_watcher_create();
|
||||
|
||||
// Libera el watcher (cierra fd / handles). Acepta nullptr.
|
||||
void file_watcher_destroy(FileWatcher* w);
|
||||
|
||||
// Registra un path (archivo o directorio). Devuelve false si no existe o no se pudo
|
||||
// anadir el watch (ej: en Linux, limite de inotify alcanzado). Tras false, llamar
|
||||
// file_watcher_last_error() para obtener detalles.
|
||||
bool file_watcher_add(FileWatcher* w, const char* path);
|
||||
|
||||
// Devuelve los eventos acumulados desde la ultima llamada. No bloqueante.
|
||||
std::vector<FileEvent> file_watcher_poll(FileWatcher* w);
|
||||
|
||||
// Devuelve el mensaje del ultimo error (vacio si no hay).
|
||||
const char* file_watcher_last_error(const FileWatcher* w);
|
||||
|
||||
} // namespace fn
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
name: file_watcher
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "fn::FileWatcher* fn::file_watcher_create(); void fn::file_watcher_destroy(fn::FileWatcher*); bool fn::file_watcher_add(fn::FileWatcher*, const char* path); std::vector<fn::FileEvent> fn::file_watcher_poll(fn::FileWatcher*); const char* fn::file_watcher_last_error(const fn::FileWatcher*)"
|
||||
description: "Watcher de archivos/directorios cross-platform (Linux inotify, Windows ReadDirectoryChangesW). API no bloqueante: registra paths con add() y consulta eventos con poll(). Cada poll() drena todos los eventos pendientes desde la llamada anterior."
|
||||
tags: [filesystem, watcher, inotify, file_events, io]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [unistd, sys/inotify, windows]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/core/file_watcher.cpp"
|
||||
params: []
|
||||
output: "FileWatcher opaco con cola interna de eventos. poll() devuelve std::vector<FileEvent> con {path, kind in {Modified, Created, Deleted}}. Errores acumulados en last_error()."
|
||||
---
|
||||
|
||||
# file_watcher
|
||||
|
||||
Watcher de archivos no bloqueante con backend nativo por plataforma. Pareja natural de `text_editor_cpp_core` para el ciclo "edita -> guarda -> recompila" sin polling de timestamps.
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace fn {
|
||||
struct FileWatcher;
|
||||
struct FileEvent {
|
||||
enum Kind { Modified, Created, Deleted };
|
||||
std::string path;
|
||||
Kind kind;
|
||||
};
|
||||
|
||||
FileWatcher* file_watcher_create();
|
||||
void file_watcher_destroy(FileWatcher*);
|
||||
bool file_watcher_add(FileWatcher*, const char* path); // file or dir
|
||||
std::vector<FileEvent> file_watcher_poll(FileWatcher*); // non-blocking, drain
|
||||
const char* file_watcher_last_error(const FileWatcher*);
|
||||
}
|
||||
```
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```cpp
|
||||
#include "core/file_watcher.h"
|
||||
|
||||
auto* fw = fn::file_watcher_create();
|
||||
fn::file_watcher_add(fw, "/tmp/shader.glsl");
|
||||
|
||||
while (running) {
|
||||
for (auto& ev : fn::file_watcher_poll(fw)) {
|
||||
switch (ev.kind) {
|
||||
case fn::FileEvent::Modified: reload(ev.path); break;
|
||||
case fn::FileEvent::Created: std::printf("created: %s\n", ev.path.c_str()); break;
|
||||
case fn::FileEvent::Deleted: std::printf("deleted: %s\n", ev.path.c_str()); break;
|
||||
}
|
||||
}
|
||||
sleep_ms(16);
|
||||
}
|
||||
|
||||
fn::file_watcher_destroy(fw);
|
||||
```
|
||||
|
||||
## Backends
|
||||
|
||||
| Plataforma | Mecanismo | Notas |
|
||||
|-----------|-----------|-------|
|
||||
| Linux | `inotify_init1(IN_NONBLOCK)` + `inotify_add_watch` | mascara: MODIFY \| CREATE \| DELETE \| CLOSE_WRITE \| MOVED_* |
|
||||
| Windows | `ReadDirectoryChangesW` overlapped + `GetOverlappedResult` no bloqueante | Para vigilar un archivo, registra el directorio padre y filtra por nombre |
|
||||
| Otros | Stub — `poll()` devuelve vector vacio | `last_error()` indica "platform not supported" |
|
||||
|
||||
## Limites y avisos
|
||||
|
||||
- **inotify watch limit (Linux)**: por defecto `fs.inotify.max_user_watches = 8192`. Si lo superas, `add()` devuelve false y `last_error()` reporta `No space left on device`. Subirlo con:
|
||||
```bash
|
||||
sudo sysctl fs.inotify.max_user_watches=524288
|
||||
```
|
||||
- **Windows directorio-level**: cuando registras un archivo, internamente se vigila el directorio padre y se filtra por nombre exacto en el poll. Eventos de archivos hermanos se descartan.
|
||||
- **No es recursivo** — `add()` registra el path dado, no su subarbol. Para vigilar un arbol, llama `add()` por cada subdirectorio (TODO si hace falta).
|
||||
- **Editor "modify" coalescing**: editores como vim escriben usando un swap + rename, lo que produce CREATE + DELETE + MOVED_TO en vez de MODIFY puro. La mascara cubre MOVED_TO para que el evento llegue como `Created` (semantica "ahora hay un archivo nuevo en esa ruta") — el caller deduplica si lo necesita.
|
||||
|
||||
## Decisiones de diseño
|
||||
|
||||
- **PIMPL**: el header no expone `inotify_event` ni `OVERLAPPED`. `FileWatcher` es opaco.
|
||||
- **No bloqueante**: el caller hace polling desde su loop principal (tipico ImGui ~60Hz). No threads, no callbacks. Mantenimiento bajo.
|
||||
- **Errores como string**: no exception throwing. `add()` devuelve `bool` y `last_error()` da contexto. `error_type: stderr_string` en frontmatter.
|
||||
- **Sin coalescing implicito**: el watcher emite todo lo que recibe del kernel. La app decide si dedup eventos cercanos en el tiempo.
|
||||
@@ -0,0 +1,68 @@
|
||||
#include "text_editor.h"
|
||||
|
||||
#include "TextEditor.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace fn {
|
||||
|
||||
struct TextEditorState {
|
||||
::TextEditor editor; // vendor type, oculto detras del PIMPL
|
||||
std::string cached_text; // buffer estable para text_editor_get_text()
|
||||
bool dirty = false;
|
||||
};
|
||||
|
||||
static const ::TextEditor::LanguageDefinition& lang_def(CodeLang lang) {
|
||||
switch (lang) {
|
||||
case CodeLang::GLSL: return ::TextEditor::LanguageDefinition::GLSL();
|
||||
case CodeLang::SQL: return ::TextEditor::LanguageDefinition::SQL();
|
||||
case CodeLang::Cpp: return ::TextEditor::LanguageDefinition::CPlusPlus();
|
||||
case CodeLang::Generic:
|
||||
default:
|
||||
return ::TextEditor::LanguageDefinition::CPlusPlus();
|
||||
}
|
||||
}
|
||||
|
||||
TextEditorState* text_editor_create(CodeLang lang) {
|
||||
auto* s = new TextEditorState();
|
||||
s->editor.SetLanguageDefinition(lang_def(lang));
|
||||
s->editor.SetShowWhitespaces(false);
|
||||
return s;
|
||||
}
|
||||
|
||||
void text_editor_destroy(TextEditorState* state) {
|
||||
delete state;
|
||||
}
|
||||
|
||||
void text_editor_set_text(TextEditorState* state, const char* text) {
|
||||
if (!state || !text) return;
|
||||
state->editor.SetText(text);
|
||||
state->cached_text = text;
|
||||
state->dirty = false;
|
||||
}
|
||||
|
||||
const char* text_editor_get_text(TextEditorState* state) {
|
||||
if (!state) return "";
|
||||
state->cached_text = state->editor.GetText();
|
||||
// El editor anade siempre un '\n' final; lo dejamos para preservar
|
||||
// la semantica del vendor (es lo que devuelve GetText()).
|
||||
return state->cached_text.c_str();
|
||||
}
|
||||
|
||||
bool text_editor_render(TextEditorState* state, const char* label, ImVec2 size) {
|
||||
if (!state) return false;
|
||||
state->editor.Render(label, size, true);
|
||||
const bool changed = state->editor.IsTextChanged();
|
||||
if (changed) state->dirty = true;
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool text_editor_is_dirty(const TextEditorState* state) {
|
||||
return state ? state->dirty : false;
|
||||
}
|
||||
|
||||
void text_editor_clear_dirty(TextEditorState* state) {
|
||||
if (state) state->dirty = false;
|
||||
}
|
||||
|
||||
} // namespace fn
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
// text_editor — wrapper PIMPL sobre ImGuiColorTextEdit (vendor MIT).
|
||||
// API en namespace fn:: que oculta el tipo concreto del vendor.
|
||||
//
|
||||
// Uso tipico:
|
||||
// auto* ed = fn::text_editor_create(fn::CodeLang::GLSL);
|
||||
// fn::text_editor_set_text(ed, "void main() { gl_FragColor = vec4(1); }");
|
||||
// if (fn::text_editor_render(ed, "##ed", {600, 400})) { /* cambio */ }
|
||||
// fn::text_editor_destroy(ed);
|
||||
//
|
||||
// El estado vive en TextEditorState (forward-declared), no se expone TextEditor del vendor.
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
namespace fn {
|
||||
|
||||
enum class CodeLang {
|
||||
Generic,
|
||||
GLSL,
|
||||
SQL,
|
||||
Cpp,
|
||||
};
|
||||
|
||||
// Forward declaration — definicion completa en text_editor.cpp (PIMPL).
|
||||
struct TextEditorState;
|
||||
|
||||
// Crea un editor con el lenguaje dado. El caller es dueno y debe llamar destroy.
|
||||
TextEditorState* text_editor_create(CodeLang lang = CodeLang::Generic);
|
||||
|
||||
// Libera el editor. Acepta nullptr (no-op).
|
||||
void text_editor_destroy(TextEditorState* state);
|
||||
|
||||
// Reemplaza el texto del editor.
|
||||
void text_editor_set_text(TextEditorState* state, const char* text);
|
||||
|
||||
// Devuelve el texto actual. El puntero es valido solo hasta la siguiente llamada
|
||||
// a text_editor_set_text/get_text/render sobre el mismo editor.
|
||||
const char* text_editor_get_text(TextEditorState* state);
|
||||
|
||||
// Renderiza el editor. Devuelve true en el frame en que el contenido cambio.
|
||||
bool text_editor_render(TextEditorState* state, const char* label, ImVec2 size);
|
||||
|
||||
// True si el editor esta marcado como "dirty" (modificado desde el ultimo clear).
|
||||
bool text_editor_is_dirty(const TextEditorState* state);
|
||||
|
||||
// Limpia el flag dirty (tipicamente tras guardar a disco).
|
||||
void text_editor_clear_dirty(TextEditorState* state);
|
||||
|
||||
} // namespace fn
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
name: text_editor
|
||||
kind: component
|
||||
lang: cpp
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "fn::TextEditorState* fn::text_editor_create(fn::CodeLang); void fn::text_editor_destroy(fn::TextEditorState*); void fn::text_editor_set_text(fn::TextEditorState*, const char*); const char* fn::text_editor_get_text(fn::TextEditorState*); bool fn::text_editor_render(fn::TextEditorState*, const char* label, ImVec2 size); bool fn::text_editor_is_dirty(const fn::TextEditorState*); void fn::text_editor_clear_dirty(fn::TextEditorState*)"
|
||||
description: "Editor de codigo embebido en ImGui con syntax highlighting (GLSL, SQL, C++, Generic). Wrapper PIMPL sobre ImGuiColorTextEdit (vendor MIT) — la API solo expone tipos opacos en namespace fn::."
|
||||
tags: [imgui, editor, text, code, glsl, sql, syntax-highlighting, tokens]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: [imgui, TextEditor]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/core/text_editor.cpp"
|
||||
framework: imgui
|
||||
source_repo: "https://github.com/BalazsJako/ImGuiColorTextEdit"
|
||||
source_license: "MIT"
|
||||
source_file: "TextEditor.h, TextEditor.cpp"
|
||||
params: []
|
||||
output: "TextEditorState opaco — encapsula el editor del vendor + buffer de texto cacheado + flag dirty. Render devuelve true cuando el contenido cambio en el frame actual."
|
||||
---
|
||||
|
||||
# text_editor
|
||||
|
||||
Editor de codigo in-app con syntax highlighting. Resuelve el ciclo de edicion sin alt-tab a un editor externo (ej: editar GLSL en `shaders_lab` y recompilar al guardar; futuro `sql_workbench` con CTAS sobre `registry.db`).
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace fn {
|
||||
enum class CodeLang { Generic, GLSL, SQL, Cpp };
|
||||
struct TextEditorState; // PIMPL — tipo opaco
|
||||
|
||||
TextEditorState* text_editor_create(CodeLang lang = CodeLang::Generic);
|
||||
void text_editor_destroy(TextEditorState*);
|
||||
|
||||
void text_editor_set_text(TextEditorState*, const char* text);
|
||||
const char* text_editor_get_text(TextEditorState*); // valido hasta el siguiente call
|
||||
bool text_editor_render(TextEditorState*, const char* label, ImVec2 size); // true si cambio
|
||||
bool text_editor_is_dirty(const TextEditorState*);
|
||||
void text_editor_clear_dirty(TextEditorState*);
|
||||
}
|
||||
```
|
||||
|
||||
## Ejemplo — editor GLSL con boton de guardado
|
||||
|
||||
```cpp
|
||||
#include "core/text_editor.h"
|
||||
#include "core/button.h"
|
||||
|
||||
static fn::TextEditorState* g_ed = nullptr;
|
||||
|
||||
void render() {
|
||||
if (!g_ed) {
|
||||
g_ed = fn::text_editor_create(fn::CodeLang::GLSL);
|
||||
fn::text_editor_set_text(g_ed,
|
||||
"#version 330\nout vec4 c;\nvoid main() { c = vec4(1); }\n");
|
||||
}
|
||||
|
||||
fn::text_editor_render(g_ed, "##editor", ImVec2(600, 400));
|
||||
|
||||
if (fn::text_editor_is_dirty(g_ed)) ImGui::TextUnformatted("(modificado)");
|
||||
|
||||
if (fn_ui::button("Save")) {
|
||||
FILE* f = std::fopen("/tmp/shader.glsl", "w");
|
||||
std::fputs(fn::text_editor_get_text(g_ed), f);
|
||||
std::fclose(f);
|
||||
fn::text_editor_clear_dirty(g_ed);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Decisiones de diseño
|
||||
|
||||
- **PIMPL** — el tipo `TextEditor` del vendor no aparece en `text_editor.h`. El header publico solo importa `imgui.h` (por `ImVec2`).
|
||||
- **`get_text` cachea** el resultado en un `std::string` propio del state para que el `const char*` siga siendo valido entre llamadas (el `GetText()` del vendor devuelve un `std::string` por valor).
|
||||
- **Sin tokens custom** — el editor hereda el theme global de ImGui (paleta interna del vendor). Para integrar `fn_tokens` haria falta crear un `Palette` propio; lo dejamos para una v2 si pesa visualmente.
|
||||
- **`Generic` reusa C++** como default: el vendor no tiene un "plain text" sin highlighting; C++ es el menos intrusivo.
|
||||
|
||||
## Lenguajes soportados
|
||||
|
||||
| `CodeLang` | Highlighting | Origen |
|
||||
|-----------|--------------|--------|
|
||||
| `GLSL` | Si | `LanguageDefinition::GLSL()` |
|
||||
| `SQL` | Si | `LanguageDefinition::SQL()` |
|
||||
| `Cpp` | Si | `LanguageDefinition::CPlusPlus()` |
|
||||
| `Generic` | C++ por defecto | `LanguageDefinition::CPlusPlus()` |
|
||||
|
||||
## Vendor
|
||||
|
||||
`cpp/vendor/imgui_text_edit/` (commit pinneado en su `README.md`). MIT.
|
||||
|
||||
## Combinacion sugerida
|
||||
|
||||
`text_editor` + `file_watcher_cpp_core` = ciclo de edicion completo. Ver `apps/primitives_gallery/demos_text_editor.cpp` para la demo combinada.
|
||||
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
# ImGuiColorTextEdit (vendored)
|
||||
|
||||
Source: https://github.com/BalazsJako/ImGuiColorTextEdit
|
||||
License: MIT
|
||||
Pinned commit: `0a88824f7de8d0bd11d8419066caa7d3469395c4` (master HEAD at vendor time)
|
||||
|
||||
Files:
|
||||
- `TextEditor.h`
|
||||
- `TextEditor.cpp`
|
||||
|
||||
Used by `cpp/functions/core/text_editor.{h,cpp}` via PIMPL — the public API is in
|
||||
`namespace fn::` and the vendor's `TextEditor` type is hidden inside the .cpp.
|
||||
|
||||
## Updating
|
||||
|
||||
```bash
|
||||
cd cpp/vendor/imgui_text_edit
|
||||
curl -fL -O https://raw.githubusercontent.com/BalazsJako/ImGuiColorTextEdit/<commit>/TextEditor.h
|
||||
curl -fL -O https://raw.githubusercontent.com/BalazsJako/ImGuiColorTextEdit/<commit>/TextEditor.cpp
|
||||
# Update commit hash above.
|
||||
```
|
||||
|
||||
## Notes / known issues
|
||||
|
||||
- The vendor expects `imgui.h` and `imgui_internal.h` in the include path. Both are
|
||||
provided by our vendored ImGui in `cpp/vendor/imgui/`.
|
||||
- `LanguageDefinition::GLSL()`, `SQL()`, `CPlusPlus()` are provided by the vendor and
|
||||
consumed by our wrapper (`fn::CodeLang`).
|
||||
- `inotify` watcher limit on Linux — see `file_watcher_cpp_core.md`.
|
||||
|
||||
## Local patches applied
|
||||
|
||||
Upstream commit `0a88824f` predates several ImGui API removals (we vendor ImGui
|
||||
1.91+). The following minimal in-tree patches were applied to `TextEditor.cpp`:
|
||||
|
||||
- `ImGui::GetKeyIndex(ImGuiKey_X)` -> `ImGuiKey_X` (the `ImGuiKey` enum is now a
|
||||
direct, stable index — `GetKeyIndex` was removed).
|
||||
- `ImGui::PushAllowKeyboardFocus(true)` / `PopAllowKeyboardFocus()` -> removed
|
||||
(replaced upstream by `SetItemKeyOwner` / `ImGuiItemFlags_AllowKeyboardFocus`,
|
||||
but the editor functions correctly without them in our embedding).
|
||||
|
||||
Re-apply when refreshing the vendor:
|
||||
|
||||
```bash
|
||||
sed -i 's/ImGui::GetKeyIndex(\([^)]*\))/\1/g' TextEditor.cpp
|
||||
sed -i 's/ImGui::PushAllowKeyboardFocus(true);/\/\/ removed in ImGui 1.89+/g' TextEditor.cpp
|
||||
sed -i 's/ImGui::PopAllowKeyboardFocus();/\/\/ removed in ImGui 1.89+/g' TextEditor.cpp
|
||||
```
|
||||
+3160
File diff suppressed because it is too large
Load Diff
+389
@@ -0,0 +1,389 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
#include <map>
|
||||
#include <regex>
|
||||
#include "imgui.h"
|
||||
|
||||
class TextEditor
|
||||
{
|
||||
public:
|
||||
enum class PaletteIndex
|
||||
{
|
||||
Default,
|
||||
Keyword,
|
||||
Number,
|
||||
String,
|
||||
CharLiteral,
|
||||
Punctuation,
|
||||
Preprocessor,
|
||||
Identifier,
|
||||
KnownIdentifier,
|
||||
PreprocIdentifier,
|
||||
Comment,
|
||||
MultiLineComment,
|
||||
Background,
|
||||
Cursor,
|
||||
Selection,
|
||||
ErrorMarker,
|
||||
Breakpoint,
|
||||
LineNumber,
|
||||
CurrentLineFill,
|
||||
CurrentLineFillInactive,
|
||||
CurrentLineEdge,
|
||||
Max
|
||||
};
|
||||
|
||||
enum class SelectionMode
|
||||
{
|
||||
Normal,
|
||||
Word,
|
||||
Line
|
||||
};
|
||||
|
||||
struct Breakpoint
|
||||
{
|
||||
int mLine;
|
||||
bool mEnabled;
|
||||
std::string mCondition;
|
||||
|
||||
Breakpoint()
|
||||
: mLine(-1)
|
||||
, mEnabled(false)
|
||||
{}
|
||||
};
|
||||
|
||||
// Represents a character coordinate from the user's point of view,
|
||||
// i. e. consider an uniform grid (assuming fixed-width font) on the
|
||||
// screen as it is rendered, and each cell has its own coordinate, starting from 0.
|
||||
// Tabs are counted as [1..mTabSize] count empty spaces, depending on
|
||||
// how many space is necessary to reach the next tab stop.
|
||||
// For example, coordinate (1, 5) represents the character 'B' in a line "\tABC", when mTabSize = 4,
|
||||
// because it is rendered as " ABC" on the screen.
|
||||
struct Coordinates
|
||||
{
|
||||
int mLine, mColumn;
|
||||
Coordinates() : mLine(0), mColumn(0) {}
|
||||
Coordinates(int aLine, int aColumn) : mLine(aLine), mColumn(aColumn)
|
||||
{
|
||||
assert(aLine >= 0);
|
||||
assert(aColumn >= 0);
|
||||
}
|
||||
static Coordinates Invalid() { static Coordinates invalid(-1, -1); return invalid; }
|
||||
|
||||
bool operator ==(const Coordinates& o) const
|
||||
{
|
||||
return
|
||||
mLine == o.mLine &&
|
||||
mColumn == o.mColumn;
|
||||
}
|
||||
|
||||
bool operator !=(const Coordinates& o) const
|
||||
{
|
||||
return
|
||||
mLine != o.mLine ||
|
||||
mColumn != o.mColumn;
|
||||
}
|
||||
|
||||
bool operator <(const Coordinates& o) const
|
||||
{
|
||||
if (mLine != o.mLine)
|
||||
return mLine < o.mLine;
|
||||
return mColumn < o.mColumn;
|
||||
}
|
||||
|
||||
bool operator >(const Coordinates& o) const
|
||||
{
|
||||
if (mLine != o.mLine)
|
||||
return mLine > o.mLine;
|
||||
return mColumn > o.mColumn;
|
||||
}
|
||||
|
||||
bool operator <=(const Coordinates& o) const
|
||||
{
|
||||
if (mLine != o.mLine)
|
||||
return mLine < o.mLine;
|
||||
return mColumn <= o.mColumn;
|
||||
}
|
||||
|
||||
bool operator >=(const Coordinates& o) const
|
||||
{
|
||||
if (mLine != o.mLine)
|
||||
return mLine > o.mLine;
|
||||
return mColumn >= o.mColumn;
|
||||
}
|
||||
};
|
||||
|
||||
struct Identifier
|
||||
{
|
||||
Coordinates mLocation;
|
||||
std::string mDeclaration;
|
||||
};
|
||||
|
||||
typedef std::string String;
|
||||
typedef std::unordered_map<std::string, Identifier> Identifiers;
|
||||
typedef std::unordered_set<std::string> Keywords;
|
||||
typedef std::map<int, std::string> ErrorMarkers;
|
||||
typedef std::unordered_set<int> Breakpoints;
|
||||
typedef std::array<ImU32, (unsigned)PaletteIndex::Max> Palette;
|
||||
typedef uint8_t Char;
|
||||
|
||||
struct Glyph
|
||||
{
|
||||
Char mChar;
|
||||
PaletteIndex mColorIndex = PaletteIndex::Default;
|
||||
bool mComment : 1;
|
||||
bool mMultiLineComment : 1;
|
||||
bool mPreprocessor : 1;
|
||||
|
||||
Glyph(Char aChar, PaletteIndex aColorIndex) : mChar(aChar), mColorIndex(aColorIndex),
|
||||
mComment(false), mMultiLineComment(false), mPreprocessor(false) {}
|
||||
};
|
||||
|
||||
typedef std::vector<Glyph> Line;
|
||||
typedef std::vector<Line> Lines;
|
||||
|
||||
struct LanguageDefinition
|
||||
{
|
||||
typedef std::pair<std::string, PaletteIndex> TokenRegexString;
|
||||
typedef std::vector<TokenRegexString> TokenRegexStrings;
|
||||
typedef bool(*TokenizeCallback)(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end, PaletteIndex & paletteIndex);
|
||||
|
||||
std::string mName;
|
||||
Keywords mKeywords;
|
||||
Identifiers mIdentifiers;
|
||||
Identifiers mPreprocIdentifiers;
|
||||
std::string mCommentStart, mCommentEnd, mSingleLineComment;
|
||||
char mPreprocChar;
|
||||
bool mAutoIndentation;
|
||||
|
||||
TokenizeCallback mTokenize;
|
||||
|
||||
TokenRegexStrings mTokenRegexStrings;
|
||||
|
||||
bool mCaseSensitive;
|
||||
|
||||
LanguageDefinition()
|
||||
: mPreprocChar('#'), mAutoIndentation(true), mTokenize(nullptr), mCaseSensitive(true)
|
||||
{
|
||||
}
|
||||
|
||||
static const LanguageDefinition& CPlusPlus();
|
||||
static const LanguageDefinition& HLSL();
|
||||
static const LanguageDefinition& GLSL();
|
||||
static const LanguageDefinition& C();
|
||||
static const LanguageDefinition& SQL();
|
||||
static const LanguageDefinition& AngelScript();
|
||||
static const LanguageDefinition& Lua();
|
||||
};
|
||||
|
||||
TextEditor();
|
||||
~TextEditor();
|
||||
|
||||
void SetLanguageDefinition(const LanguageDefinition& aLanguageDef);
|
||||
const LanguageDefinition& GetLanguageDefinition() const { return mLanguageDefinition; }
|
||||
|
||||
const Palette& GetPalette() const { return mPaletteBase; }
|
||||
void SetPalette(const Palette& aValue);
|
||||
|
||||
void SetErrorMarkers(const ErrorMarkers& aMarkers) { mErrorMarkers = aMarkers; }
|
||||
void SetBreakpoints(const Breakpoints& aMarkers) { mBreakpoints = aMarkers; }
|
||||
|
||||
void Render(const char* aTitle, const ImVec2& aSize = ImVec2(), bool aBorder = false);
|
||||
void SetText(const std::string& aText);
|
||||
std::string GetText() const;
|
||||
|
||||
void SetTextLines(const std::vector<std::string>& aLines);
|
||||
std::vector<std::string> GetTextLines() const;
|
||||
|
||||
std::string GetSelectedText() const;
|
||||
std::string GetCurrentLineText()const;
|
||||
|
||||
int GetTotalLines() const { return (int)mLines.size(); }
|
||||
bool IsOverwrite() const { return mOverwrite; }
|
||||
|
||||
void SetReadOnly(bool aValue);
|
||||
bool IsReadOnly() const { return mReadOnly; }
|
||||
bool IsTextChanged() const { return mTextChanged; }
|
||||
bool IsCursorPositionChanged() const { return mCursorPositionChanged; }
|
||||
|
||||
bool IsColorizerEnabled() const { return mColorizerEnabled; }
|
||||
void SetColorizerEnable(bool aValue);
|
||||
|
||||
Coordinates GetCursorPosition() const { return GetActualCursorCoordinates(); }
|
||||
void SetCursorPosition(const Coordinates& aPosition);
|
||||
|
||||
inline void SetHandleMouseInputs (bool aValue){ mHandleMouseInputs = aValue;}
|
||||
inline bool IsHandleMouseInputsEnabled() const { return mHandleKeyboardInputs; }
|
||||
|
||||
inline void SetHandleKeyboardInputs (bool aValue){ mHandleKeyboardInputs = aValue;}
|
||||
inline bool IsHandleKeyboardInputsEnabled() const { return mHandleKeyboardInputs; }
|
||||
|
||||
inline void SetImGuiChildIgnored (bool aValue){ mIgnoreImGuiChild = aValue;}
|
||||
inline bool IsImGuiChildIgnored() const { return mIgnoreImGuiChild; }
|
||||
|
||||
inline void SetShowWhitespaces(bool aValue) { mShowWhitespaces = aValue; }
|
||||
inline bool IsShowingWhitespaces() const { return mShowWhitespaces; }
|
||||
|
||||
void SetTabSize(int aValue);
|
||||
inline int GetTabSize() const { return mTabSize; }
|
||||
|
||||
void InsertText(const std::string& aValue);
|
||||
void InsertText(const char* aValue);
|
||||
|
||||
void MoveUp(int aAmount = 1, bool aSelect = false);
|
||||
void MoveDown(int aAmount = 1, bool aSelect = false);
|
||||
void MoveLeft(int aAmount = 1, bool aSelect = false, bool aWordMode = false);
|
||||
void MoveRight(int aAmount = 1, bool aSelect = false, bool aWordMode = false);
|
||||
void MoveTop(bool aSelect = false);
|
||||
void MoveBottom(bool aSelect = false);
|
||||
void MoveHome(bool aSelect = false);
|
||||
void MoveEnd(bool aSelect = false);
|
||||
|
||||
void SetSelectionStart(const Coordinates& aPosition);
|
||||
void SetSelectionEnd(const Coordinates& aPosition);
|
||||
void SetSelection(const Coordinates& aStart, const Coordinates& aEnd, SelectionMode aMode = SelectionMode::Normal);
|
||||
void SelectWordUnderCursor();
|
||||
void SelectAll();
|
||||
bool HasSelection() const;
|
||||
|
||||
void Copy();
|
||||
void Cut();
|
||||
void Paste();
|
||||
void Delete();
|
||||
|
||||
bool CanUndo() const;
|
||||
bool CanRedo() const;
|
||||
void Undo(int aSteps = 1);
|
||||
void Redo(int aSteps = 1);
|
||||
|
||||
static const Palette& GetDarkPalette();
|
||||
static const Palette& GetLightPalette();
|
||||
static const Palette& GetRetroBluePalette();
|
||||
|
||||
private:
|
||||
typedef std::vector<std::pair<std::regex, PaletteIndex>> RegexList;
|
||||
|
||||
struct EditorState
|
||||
{
|
||||
Coordinates mSelectionStart;
|
||||
Coordinates mSelectionEnd;
|
||||
Coordinates mCursorPosition;
|
||||
};
|
||||
|
||||
class UndoRecord
|
||||
{
|
||||
public:
|
||||
UndoRecord() {}
|
||||
~UndoRecord() {}
|
||||
|
||||
UndoRecord(
|
||||
const std::string& aAdded,
|
||||
const TextEditor::Coordinates aAddedStart,
|
||||
const TextEditor::Coordinates aAddedEnd,
|
||||
|
||||
const std::string& aRemoved,
|
||||
const TextEditor::Coordinates aRemovedStart,
|
||||
const TextEditor::Coordinates aRemovedEnd,
|
||||
|
||||
TextEditor::EditorState& aBefore,
|
||||
TextEditor::EditorState& aAfter);
|
||||
|
||||
void Undo(TextEditor* aEditor);
|
||||
void Redo(TextEditor* aEditor);
|
||||
|
||||
std::string mAdded;
|
||||
Coordinates mAddedStart;
|
||||
Coordinates mAddedEnd;
|
||||
|
||||
std::string mRemoved;
|
||||
Coordinates mRemovedStart;
|
||||
Coordinates mRemovedEnd;
|
||||
|
||||
EditorState mBefore;
|
||||
EditorState mAfter;
|
||||
};
|
||||
|
||||
typedef std::vector<UndoRecord> UndoBuffer;
|
||||
|
||||
void ProcessInputs();
|
||||
void Colorize(int aFromLine = 0, int aCount = -1);
|
||||
void ColorizeRange(int aFromLine = 0, int aToLine = 0);
|
||||
void ColorizeInternal();
|
||||
float TextDistanceToLineStart(const Coordinates& aFrom) const;
|
||||
void EnsureCursorVisible();
|
||||
int GetPageSize() const;
|
||||
std::string GetText(const Coordinates& aStart, const Coordinates& aEnd) const;
|
||||
Coordinates GetActualCursorCoordinates() const;
|
||||
Coordinates SanitizeCoordinates(const Coordinates& aValue) const;
|
||||
void Advance(Coordinates& aCoordinates) const;
|
||||
void DeleteRange(const Coordinates& aStart, const Coordinates& aEnd);
|
||||
int InsertTextAt(Coordinates& aWhere, const char* aValue);
|
||||
void AddUndo(UndoRecord& aValue);
|
||||
Coordinates ScreenPosToCoordinates(const ImVec2& aPosition) const;
|
||||
Coordinates FindWordStart(const Coordinates& aFrom) const;
|
||||
Coordinates FindWordEnd(const Coordinates& aFrom) const;
|
||||
Coordinates FindNextWord(const Coordinates& aFrom) const;
|
||||
int GetCharacterIndex(const Coordinates& aCoordinates) const;
|
||||
int GetCharacterColumn(int aLine, int aIndex) const;
|
||||
int GetLineCharacterCount(int aLine) const;
|
||||
int GetLineMaxColumn(int aLine) const;
|
||||
bool IsOnWordBoundary(const Coordinates& aAt) const;
|
||||
void RemoveLine(int aStart, int aEnd);
|
||||
void RemoveLine(int aIndex);
|
||||
Line& InsertLine(int aIndex);
|
||||
void EnterCharacter(ImWchar aChar, bool aShift);
|
||||
void Backspace();
|
||||
void DeleteSelection();
|
||||
std::string GetWordUnderCursor() const;
|
||||
std::string GetWordAt(const Coordinates& aCoords) const;
|
||||
ImU32 GetGlyphColor(const Glyph& aGlyph) const;
|
||||
|
||||
void HandleKeyboardInputs();
|
||||
void HandleMouseInputs();
|
||||
void Render();
|
||||
|
||||
float mLineSpacing;
|
||||
Lines mLines;
|
||||
EditorState mState;
|
||||
UndoBuffer mUndoBuffer;
|
||||
int mUndoIndex;
|
||||
|
||||
int mTabSize;
|
||||
bool mOverwrite;
|
||||
bool mReadOnly;
|
||||
bool mWithinRender;
|
||||
bool mScrollToCursor;
|
||||
bool mScrollToTop;
|
||||
bool mTextChanged;
|
||||
bool mColorizerEnabled;
|
||||
float mTextStart; // position (in pixels) where a code line starts relative to the left of the TextEditor.
|
||||
int mLeftMargin;
|
||||
bool mCursorPositionChanged;
|
||||
int mColorRangeMin, mColorRangeMax;
|
||||
SelectionMode mSelectionMode;
|
||||
bool mHandleKeyboardInputs;
|
||||
bool mHandleMouseInputs;
|
||||
bool mIgnoreImGuiChild;
|
||||
bool mShowWhitespaces;
|
||||
|
||||
Palette mPaletteBase;
|
||||
Palette mPalette;
|
||||
LanguageDefinition mLanguageDefinition;
|
||||
RegexList mRegexList;
|
||||
|
||||
bool mCheckComments;
|
||||
Breakpoints mBreakpoints;
|
||||
ErrorMarkers mErrorMarkers;
|
||||
ImVec2 mCharAdvance;
|
||||
Coordinates mInteractiveStart, mInteractiveEnd;
|
||||
std::string mLineBuffer;
|
||||
uint64_t mStartTime;
|
||||
|
||||
float mLastClick;
|
||||
};
|
||||
Reference in New Issue
Block a user