fix(infra): gradle_run detecta android-sdk — issue 0076 #2

Open
dataforge wants to merge 538 commits from auto/0076-gradle-sdk-detect into master
24 changed files with 6041 additions and 0 deletions
Showing only changes of commit e5d2201377 - Show all commits
+13
View File
@@ -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()
+159
View File
@@ -0,0 +1,159 @@
# primitives_gallery
Catalogo visual interactivo de los primitivos UI del registry (`cpp/functions/core` y `cpp/functions/viz`). Un solo ejecutable con sidebar izquierdo + panel derecho que renderiza la demo del primitivo seleccionado con todas sus variantes y un snippet de codigo.
## Rol
| Funcion | Como lo cumple |
|---|---|
| Smoke test visual | Abrir la gallery tras un cambio en tokens / componentes; si algo se ve raro, lo cazas en segundos. |
| Documentacion viva | Cada demo muestra el componente trabajando + el snippet exacto. Mas rapido que leer los `.md`. |
| Build gate | Esta en el CMake principal (`cpp/CMakeLists.txt`). Si un primitivo rompe API, la gallery no compila => CI rojo. |
| Sandbox de prototipos | Datos sinteticos, sin backend; ideal para iterar un primitivo nuevo sin tocar el dashboard. |
## Build & run
```bash
# Linux
cmake --build cpp/build/linux --target primitives_gallery -j$(nproc)
./cpp/build/linux/apps/primitives_gallery/primitives_gallery
# Windows (cross-compile)
cmake --build cpp/build/windows --target primitives_gallery -j$(nproc)
# binario: cpp/build/windows/apps/primitives_gallery/primitives_gallery.exe
```
No se conecta a `sqlite_api` ni a ningun backend. Datos sinteticos generados in-memory.
## Demos disponibles
### Core
| Demo | Primitivo | Que muestra |
|---|---|---|
| button | `button_cpp_core` | 4 variantes x 3 sizes |
| icon_button | `icon_button_cpp_core` | Glyphs comunes con tooltip |
| toolbar | `toolbar_cpp_core` | Dos grupos con separador vertical |
| modal_dialog | `modal_dialog_cpp_core` | Boton que abre modal con form |
| text_input | `text_input_cpp_core` | 3 inputs con placeholder |
| select | `select_cpp_core` | Dropdown con y sin `(none)` |
| toast + inbox | `toast_cpp_core` (v1.1) | 4 botones que disparan toasts + campana con badge |
| tree_view | `tree_view_cpp_core` | Arbol fake de proyectos -> apps |
| badge | `badge_cpp_core` | 6 variantes semanticas |
| empty_state | `empty_state_cpp_core` | Lista vacia con icono + cta |
| page_header | `page_header_cpp_core` | Header con toolbar a la derecha |
| dashboard_panel | `dashboard_panel_cpp_core` | Panel con titulo y borde |
| kpi_card | `kpi_card_cpp_viz` (v1.2) | Grid 1x4 con sparklines y delta |
### Viz
| Demo | Primitivo | Que muestra |
|---|---|---|
| bar_chart | `bar_chart_cpp_viz` (v1.2) | Labels que caben + labels rotados 45 |
| pie_chart | `pie_chart_cpp_viz` (v1.1) | Pie + donut con tooltip por slice |
| line_plot | `line_plot_cpp_viz` (v1.1) | Serie sintetica `sin(t) + ruido` |
| scatter_plot | `scatter_plot_cpp_viz` (v1.1) | 120 puntos con correlacion |
| histogram | `histogram_cpp_viz` (v1.1) | 300 muestras gaussianas |
| sparkline | `sparkline_cpp_viz` | Trending up / down / flat |
| graph_viewport | `graph_viewport_cpp_viz` | **Ver seccion abajo** |
## Demo `graph_viewport` (en detalle)
Pipeline completo de visualizacion de grafos con instanced GPU rendering:
- `graph_renderer_cpp_viz` (1 draw call para todos los nodos via `glDrawArraysInstanced`)
- `graph_force_layout_cpp_viz` (Barnes-Hut, paso de simulacion por frame)
- `graph_spatial_hash_cpp_core` (hit-testing O(1) bajo el cursor)
- `graph_viewport_cpp_viz` (widget que orquesta los anteriores con pan/zoom/select)
### Controles
| Control | Rango | Efecto |
|---|---|---|
| `Nodes` | 100 20 000 | Numero de nodos a generar |
| `Clusters` | 2 16 | Numero de comunidades (cada una con su color) |
| `Repulsion` | 100 20 000 | Fuerza repulsiva entre todos los nodos. Mas alto => grafo mas extendido y energia mayor. |
| `Attraction` | 0.001 0.5 | Constante del muelle de las aristas. Mas alto => clusters mas compactos. |
| `Gravity` | 0.0 0.05 | Tiron hacia (0,0). Util para evitar drift cuando subes mucho la repulsion. |
| `Regenerate` | boton | Regenera el grafo con los valores actuales de Nodes/Clusters. |
| `Pause / Resume layout` | boton | Para o reanuda la simulacion force-directed. |
| `Fit view` | boton | Encuadra la camara al bounding box del grafo con 10% de padding. |
Los tres sliders de fuerzas se leen cada frame y se inyectan en `ForceLayoutConfig`, asi que cambiar un valor durante el layout en marcha re-calibra el sistema al instante.
### Stats line (sin vibracion)
Una sola linea fija — sin secciones condicionales que cambien la altura del panel:
```
nodes=N edges=E energy=X fps=F | hover=#id cN sel=#id
```
`hover` y `sel` muestran `-` cuando no hay nada seleccionado para mantener el ancho/alto estable; antes una fila condicional desplazaba el viewport en cada hover.
### Interaccion con el viewport
| Gesto | Accion |
|---|---|
| Drag con boton izquierdo en zona vacia | Pan de camara |
| Wheel | Zoom (limites 0.01x 50x) |
| Drag sobre nodo | Mueve el nodo (lo `pin`ea durante el drag) |
| Click sobre nodo | Selecciona (`s_state.selected_node`) |
| Hover sobre nodo | Resaltado + `s_state.hovered_node` poblado |
### Datos sinteticos
`generate_synthetic_graph(N, K)` reparte N nodos en K clusters dispuestos en circulo, con ~3 aristas intra-cluster por nodo y un 5% adicional de aristas inter-cluster. Paleta de 8 colores ABGR. Posiciones iniciales con dispersion gaussiana de 80 px alrededor del centroide del cluster — el force layout las reordena en pocos frames.
### Performance esperada
| Nodes | FPS objetivo (RTX 30xx, viewport 800x460) | Notas |
|---|---|---|
| 1 000 | 60 (vsync) | Caso comun; layout converge < 1 s |
| 5 000 | 60 | Pipeline al limite del CPU para Barnes-Hut |
| 20 000 | 30 50 | El cuello pasa a ser el layout (CPU); GPU render sigue holgado |
Si necesitas mas, fija los nodos (`pinned = true` o `Pause layout`) y veras 60 fps estables — el bottleneck es la simulacion, no el render.
## Anadir un demo nuevo
1. Anadir el prototipo en `demos.h` dentro de `namespace gallery`:
```cpp
void demo_my_thing();
```
2. Implementar el cuerpo en `demos_core.cpp` o `demos_viz.cpp` (o un fichero nuevo si la demo es grande, p.ej. `demos_graph.cpp`).
3. Registrar la entrada en el array `k_demos[]` de `main.cpp`:
```cpp
{"my_thing", "my_thing", "Core" /* o "Viz" */, &gallery::demo_my_thing},
```
4. Si la demo necesita `.cpp` adicionales del registry, anadirlos a `CMakeLists.txt` de la gallery.
5. Recompilar.
## Estructura
```
cpp/apps/primitives_gallery/
CMakeLists.txt # target primitives_gallery
README.md # este fichero
main.cpp # sidebar + router
demo.{h,cpp} # helpers (demo_header, section, code_block, ...)
demos.h # prototipos void demo_xxx()
demos_core.cpp # demos del dominio core
demos_viz.cpp # demos del dominio viz (charts simples)
demos_graph.cpp # demo de graph_viewport (mas pesada, fichero aparte)
```
## Convenciones para los demos
- **Sin estado real**: usar arrays sinteticos (`float fake[] = {...}`) o generadores deterministas con seed fijo. Datos reproducibles.
- **Sin red**: nunca llamar a `sqlite_api`, HTTP, filesystem. La gallery debe arrancar offline en cualquier maquina.
- **Snippets honestos**: el `code_block(...)` debe mostrar el codigo que produce esa demo, no pseudocodigo.
- **Variantes en grids**: si un primitivo tiene N variantes x M tamanos, mostrarlos todos en un `BeginTable` para comparacion lado-a-lado.
- **Estado static**: si la demo es interactiva (sliders, modal, etc.), guardar el estado en `static` locales — la gallery no destruye demos al cambiar de seccion, asi que el estado persiste hasta cerrar la app.
## Iconos en los demos
A partir de la sesion 2026-04-25 los demos usan los macros `TI_*` de `cpp/functions/core/icons_tabler.h` (Tabler v3.41.1, 5093 glyphs). La fuente la carga automaticamente `fn::run_app` via `icon_font_cpp_core`, y `add_imgui_app` copia `tabler-icons.ttf` junto al ejecutable post-build (no hay paso manual).
`demo_icon_button` y `demo_toolbar` (en `demos_core.cpp`) son la referencia visual: muestran el patron `button(TI_PLUS " New", V::Primary)` y la fila de iconos sueltos. Ver `cpp/DESIGN_SYSTEM.md` seccion 11 para la regla.
Si añades un demo nuevo y necesitas glyphs, **no metas `\x..` UTF-8 inline** — busca el icono en `icons_tabler.h` (o en https://tabler.io/icons) y usa el `TI_*` correspondiente.
+76
View File
@@ -0,0 +1,76 @@
#include "demo.h"
#include "core/tokens.h"
#include <cstdio>
namespace gallery {
void demo_header(const char* name, const char* version, const char* description) {
using namespace fn_tokens;
ImGui::SetWindowFontScale(1.4f);
ImGui::TextUnformatted(name);
ImGui::SetWindowFontScale(1.0f);
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::Text(" %s", version);
ImGui::PopStyleColor();
if (description && *description) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::TextWrapped("%s", description);
ImGui::PopStyleColor();
}
ImGui::Separator();
ImGui::Dummy(ImVec2(0, spacing::sm));
}
void section(const char* title) {
using namespace fn_tokens;
ImGui::Dummy(ImVec2(0, spacing::sm));
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::TextUnformatted(title);
ImGui::PopStyleColor();
ImGui::Separator();
ImGui::Dummy(ImVec2(0, spacing::xs));
}
void variant_label(const char* text) {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim);
ImGui::TextUnformatted(text);
ImGui::PopStyleColor();
}
void code_block(const char* code) {
using namespace fn_tokens;
ImGui::Dummy(ImVec2(0, spacing::sm));
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::TextUnformatted("// example");
ImGui::PopStyleColor();
ImGui::PushStyleColor(ImGuiCol_ChildBg, colors::bg);
ImGui::PushStyleColor(ImGuiCol_Border, colors::border);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, radius::sm);
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::md, spacing::sm));
// Altura: aprox lineas * line-height
int lines = 1;
for (const char* p = code; *p; ++p) if (*p == '\n') ++lines;
float h = lines * ImGui::GetTextLineHeightWithSpacing() + spacing::md;
char id[32];
std::snprintf(id, sizeof(id), "##code_%p", (const void*)code);
ImGui::BeginChild(id, ImVec2(0, h),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PushStyleColor(ImGuiCol_Text, colors::text);
ImGui::TextUnformatted(code);
ImGui::PopStyleColor();
ImGui::EndChild();
ImGui::PopStyleVar(3);
ImGui::PopStyleColor(2);
}
} // namespace gallery
+22
View File
@@ -0,0 +1,22 @@
#pragma once
// Helpers compartidos por todas las demos de la gallery.
// No son primitivos del registry — son utilidades locales de este app.
#include "imgui.h"
#include <string>
namespace gallery {
// Titulo + version + descripcion en la parte superior del panel derecho.
void demo_header(const char* name, const char* version, const char* description);
// Seccion secundaria dentro de una demo (agrupar variantes).
void section(const char* title);
// Bloque de codigo monoespaciado con bg surface y label "// example".
void code_block(const char* code);
// Etiqueta sutil encima de un grupo de widgets.
void variant_label(const char* text);
} // namespace gallery
+37
View File
@@ -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
+447
View File
@@ -0,0 +1,447 @@
#include "demos.h"
#include "demo.h"
#include "core/button.h"
#include "core/icon_button.h"
#include "core/toolbar.h"
#include "core/modal_dialog.h"
#include "core/text_input.h"
#include "core/select.h"
#include "core/toast.h"
#include "core/tree_view.h"
#include "core/badge.h"
#include "core/empty_state.h"
#include "core/page_header.h"
#include "core/dashboard_panel.h"
#include "core/tokens.h"
#include "core/icons_tabler.h"
#include "viz/kpi_card.h"
#include <imgui.h>
#include <cstdio>
using namespace fn_ui;
using V = ButtonVariant;
using S = ButtonSize;
namespace gallery {
// ---------------------------------------------------------------------------
// button
// ---------------------------------------------------------------------------
void demo_button() {
demo_header("button", "v1.0.0",
"Boton con 4 variantes semanticas y 3 tamanos. Usa tokens para colores, "
"radius y padding — estilo consistente en toda la app.");
section("Variants x Sizes");
const V variants[] = {V::Primary, V::Secondary, V::Subtle, V::Danger};
const char* variant_names[] = {"Primary", "Secondary", "Subtle", "Danger"};
const S sizes[] = {S::Sm, S::Md, S::Lg};
const char* size_names[] = {"sm", "md", "lg"};
if (ImGui::BeginTable("##btn_grid", 5, ImGuiTableFlags_SizingFixedFit)) {
ImGui::TableSetupColumn("size");
for (int c = 0; c < 4; c++) ImGui::TableSetupColumn(variant_names[c]);
ImGui::TableHeadersRow();
for (int s = 0; s < 3; s++) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
variant_label(size_names[s]);
for (int v = 0; v < 4; v++) {
ImGui::TableSetColumnIndex(v + 1);
char id[32];
std::snprintf(id, sizeof(id), "%s##%d%d", variant_names[v], s, v);
button(id, variants[v], sizes[s]);
}
}
ImGui::EndTable();
}
code_block(
"#include \"core/button.h\"\n"
"using fn_ui::button;\n"
"using V = fn_ui::ButtonVariant;\n\n"
"if (button(\"Save\", V::Primary)) save();\n"
"if (button(\"Cancel\", V::Subtle)) close();\n"
"if (button(\"Delete\", V::Danger)) confirm();"
);
}
// ---------------------------------------------------------------------------
// icon_button
// ---------------------------------------------------------------------------
void demo_icon_button() {
demo_header("icon_button", "v1.0.0",
"Boton cuadrado 28x28 con un glyph centrado y tooltip opcional. "
"Usa los TI_* de core/icons_tabler.h (Tabler Icons cargado automaticamente "
"por fn::run_app via icon_font.cpp).");
section("Tabler icon set");
struct { const char* id; const char* glyph; const char* tip; } ic[] = {
{"##rl", TI_REFRESH, "Reload"},
{"##ad", TI_PLUS, "Add"},
{"##dl", TI_TRASH, "Delete"},
{"##dn", TI_CHEVRON_DOWN, "Dropdown"},
{"##cf", TI_SETTINGS, "Settings"},
{"##ok", TI_CHECK, "Check"},
{"##cl", TI_X, "Close"},
{"##ed", TI_PENCIL, "Edit"},
{"##sv", TI_DEVICE_FLOPPY, "Save"},
{"##sr", TI_SEARCH, "Search"},
{"##hp", TI_HELP, "Help"},
{"##hm", TI_HOME, "Home"},
};
for (auto& b : ic) {
icon_button(b.id, b.glyph, b.tip);
ImGui::SameLine();
}
ImGui::NewLine();
code_block(
"#include \"core/icons_tabler.h\"\n\n"
"if (icon_button(\"##reload\", TI_REFRESH, \"Reload\"))\n"
" reload_data();\n\n"
"// Mas de 5000 iconos disponibles — ver core/icons_tabler.h"
);
}
// ---------------------------------------------------------------------------
// toolbar
// ---------------------------------------------------------------------------
void demo_toolbar() {
demo_header("toolbar", "v1.0.0",
"Grupo horizontal con spacing consistente y separadores verticales sutiles. "
"El caller usa ImGui::SameLine entre items y toolbar_separator entre grupos.");
section("Example with two groups");
toolbar_begin();
button(TI_PLUS " New", V::Primary); ImGui::SameLine();
button(TI_FOLDER_OPEN " Open", V::Secondary); ImGui::SameLine();
button(TI_DEVICE_FLOPPY " Save",V::Secondary);
toolbar_separator();
icon_button("##set", TI_SETTINGS, "Settings");
ImGui::SameLine();
icon_button("##help", TI_HELP, "Help");
toolbar_end();
code_block(
"#include \"core/icons_tabler.h\"\n\n"
"toolbar_begin();\n"
" button(TI_PLUS \" New\", V::Primary); ImGui::SameLine();\n"
" button(TI_FOLDER_OPEN \" Open\", V::Secondary);\n"
" toolbar_separator();\n"
" icon_button(\"##set\", TI_SETTINGS, \"Settings\");\n"
"toolbar_end();"
);
}
// ---------------------------------------------------------------------------
// modal_dialog
// ---------------------------------------------------------------------------
void demo_modal() {
demo_header("modal_dialog", "v1.0.0",
"Popup modal centrada con estilo surface+border. Close con Escape o click en X. "
"Patron begin/end — modal_dialog_end debe llamarse siempre.");
static bool show = false;
if (button("Open modal", V::Primary)) show = true;
if (modal_dialog_begin("Demo modal", &show, ImVec2(380, 0))) {
ImGui::TextWrapped(
"Modal centrada en el viewport principal, con estilo tokens.");
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm));
static char buf[64] = {};
text_input("Name", buf, sizeof(buf), "escribe algo");
ImGui::Separator();
if (button("Cancel", V::Subtle)) show = false;
ImGui::SameLine();
if (button("Done", V::Primary)) show = false;
}
modal_dialog_end();
code_block(
"static bool show = false;\n"
"if (button(\"Open\", Primary)) show = true;\n"
"if (modal_dialog_begin(\"Title\", &show, ImVec2(380,0))) {\n"
" // ... campos del form ...\n"
" if (button(\"Done\", Primary)) show = false;\n"
"}\n"
"modal_dialog_end();"
);
}
// ---------------------------------------------------------------------------
// text_input
// ---------------------------------------------------------------------------
void demo_text_input() {
demo_header("text_input", "v1.0.0",
"Label muted + input estilizado con tokens. Full-width dentro del contenedor. "
"Placeholder opcional mostrado en text_dim cuando el buffer esta vacio.");
static char name[128] = {};
static char desc[256] = {};
static char tags[128] = {};
ImGui::BeginChild("##ti_wrap", ImVec2(420, 0), ImGuiChildFlags_AutoResizeY);
text_input("Name", name, sizeof(name), "my-new-thing");
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
text_input("Description", desc, sizeof(desc));
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
text_input("Tags (CSV)", tags, sizeof(tags), "imgui,ui,form");
ImGui::EndChild();
code_block(
"static char name[128] = {};\n"
"text_input(\"Name\", name, sizeof(name), \"my-new-thing\");\n"
"// true on change — se usa mas para validar en vivo\n"
"// que para leer el valor (que vive en el buffer)."
);
}
// ---------------------------------------------------------------------------
// select
// ---------------------------------------------------------------------------
void demo_select() {
demo_header("select", "v1.0.0",
"Dropdown con label muted y opcion (none) opcional. Mismo estilo tokens que text_input.");
static int lang_idx = 0;
static int domain_idx = -1;
const char* langs[] = {"go", "py", "ts", "sh", "cpp"};
const char* domains[] = {"core", "infra", "finance", "datascience", "viz"};
ImGui::BeginChild("##sl_wrap", ImVec2(420, 0), ImGuiChildFlags_AutoResizeY);
select("Language", &lang_idx, langs, 5);
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
select("Domain (optional)", &domain_idx, domains, 5, true);
ImGui::EndChild();
code_block(
"static int lang = 0;\n"
"const char* langs[] = {\"go\",\"py\",\"ts\",\"sh\",\"cpp\"};\n"
"select(\"Language\", &lang, langs, 5);"
);
}
// ---------------------------------------------------------------------------
// toast + inbox
// ---------------------------------------------------------------------------
void demo_toast() {
demo_header("toast", "v1.1.0",
"Notificaciones efimeras (~3.5s con fade-out) + inbox con campana. "
"La campana muestra badge con no-leidos y popover con las ultimas 50.");
section("Trigger toasts");
if (button("Info", V::Secondary)) toast_push(ToastKind::Info, "this is an info toast");
ImGui::SameLine();
if (button("Success", V::Primary)) toast_push(ToastKind::Success, "operation completed");
ImGui::SameLine();
if (button("Warning", V::Secondary)) toast_push(ToastKind::Warning, "heads up about something");
ImGui::SameLine();
if (button("Error", V::Danger)) toast_push(ToastKind::Error, "operation failed: reason");
section("Inbox (bell with unread badge)");
toast_inbox_button("##inbox_demo");
code_block(
"toast_push(ToastKind::Success, \"Reindexed 891 functions\");\n"
"toast_push(ToastKind::Error, \"HTTP 503: server down\");\n\n"
"// En la toolbar:\n"
"toast_inbox_button(\"##inbox\");\n\n"
"// Una vez por frame al final del render:\n"
"toast_render();"
);
}
// ---------------------------------------------------------------------------
// tree_view
// ---------------------------------------------------------------------------
void demo_tree_view() {
demo_header("tree_view", "v1.0.0",
"Tree low-level para jerarquias (ej. projects -> apps/analysis/vaults). "
"Sin estado interno: el caller gestiona seleccion y pasa 'selected' por parametro.");
static std::string selected;
section("Projects (fake)");
ImGui::BeginChild("##tv", ImVec2(360, 200), ImGuiChildFlags_Borders);
struct FakeProject { const char* id; const char* name; const char* apps[3]; };
const FakeProject projs[] = {
{"app_turismo", "app_turismo", {"guide_es", "offline_maps", nullptr}},
{"element_agents", "element_agents", {"matrix_bot", nullptr, nullptr}},
{"fn_monitoring", "fn_monitoring", {"sqlite_api", "registry_dashboard", nullptr}},
};
for (auto& p : projs) {
bool sel = (selected == p.id);
if (tree_branch_begin(p.id, p.name, sel)) {
if (tree_node_clicked()) selected = p.id;
for (int i = 0; i < 3 && p.apps[i]; i++) {
bool asel = (selected == p.apps[i]);
tree_leaf(p.apps[i], p.apps[i], asel);
if (tree_node_clicked()) selected = p.apps[i];
}
tree_branch_end();
} else if (tree_node_clicked()) {
selected = p.id;
}
}
ImGui::EndChild();
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
ImGui::Text("Selected: %s", selected.empty() ? "(none)" : selected.c_str());
ImGui::PopStyleColor();
code_block(
"static std::string sel;\n"
"if (tree_branch_begin(p.id, p.name, sel == p.id)) {\n"
" if (tree_node_clicked()) sel = p.id;\n"
" for (auto& a : p.apps) {\n"
" tree_leaf(a.id, a.name, sel == a.id);\n"
" if (tree_node_clicked()) sel = a.id;\n"
" }\n"
" tree_branch_end();\n"
"}"
);
}
// ---------------------------------------------------------------------------
// kpi_card
// ---------------------------------------------------------------------------
void demo_kpi_card() {
demo_header("kpi_card", "v1.3.0",
"Card compacta 86px con icono opcional + label muted, valor x1.4, trend con "
"TI_TRENDING_UP/DOWN y sparkline. Usa tokens: surface bg, border, radius md.");
if (ImGui::BeginTable("##kpi_grid", 4, ImGuiTableFlags_SizingStretchSame)) {
float history[] = {10, 12, 11, 15, 18, 17, 20};
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); kpi_card("Revenue", 20000.0f, 12.5f, history, 7, "$%.0f", TI_CASH);
ImGui::TableSetColumnIndex(1); kpi_card("Users", 1250.0f, 3.4f, history, 7, "%.0f", TI_USERS);
ImGui::TableSetColumnIndex(2); kpi_card("Churn", 2.1f, -0.3f, history, 7, "%.1f%%", TI_CHART_BAR);
ImGui::TableSetColumnIndex(3); kpi_card("Errors", 0.0f, 0.0f, nullptr, 0, "%.0f", TI_ALERT_CIRCLE);
ImGui::EndTable();
}
code_block(
"#include \"core/icons_tabler.h\"\n\n"
"float history[] = {10,12,11,15,18,17,20};\n"
"kpi_card(\"Revenue\", 20000.0f, 12.5f, history, 7, \"$%.0f\", TI_CASH);\n"
"kpi_card(\"Users\", 1250.0f, 3.4f, history, 7, \"%.0f\", TI_USERS);\n"
"// Sin delta ni history: muestra TI_MINUS como placeholder\n"
"kpi_card(\"Errors\", 0.0f, 0.0f, nullptr, 0, \"%.0f\", TI_ALERT_CIRCLE);"
);
}
// ---------------------------------------------------------------------------
// badge
// ---------------------------------------------------------------------------
void demo_badge() {
demo_header("badge", "v1.0.0",
"Etiqueta inline con 6 variantes semanticas. Equivalente a <Badge> de fn_library.");
section("Variants");
badge("Default", BadgeVariant::Default); ImGui::SameLine();
badge("Success", BadgeVariant::Success); ImGui::SameLine();
badge("Warning", BadgeVariant::Warning); ImGui::SameLine();
badge("Error", BadgeVariant::Error); ImGui::SameLine();
badge("Info", BadgeVariant::Info); ImGui::SameLine();
badge("Outline", BadgeVariant::Outline);
section("In context (table row)");
ImGui::Text("filter_slice_go_core"); ImGui::SameLine();
badge("pure", BadgeVariant::Success); ImGui::SameLine();
badge("tested", BadgeVariant::Info);
code_block(
"badge(\"pure\", BadgeVariant::Success);\n"
"badge(\"stale\", BadgeVariant::Warning);\n"
"badge(\"broken\", BadgeVariant::Error);"
);
}
// ---------------------------------------------------------------------------
// empty_state
// ---------------------------------------------------------------------------
void demo_empty_state() {
demo_header("empty_state", "v1.0.0",
"Icono grande muted + titulo + descripcion opcional. Para listas/tablas vacias.");
ImGui::BeginChild("##es", ImVec2(0, 180), ImGuiChildFlags_Borders);
empty_state("( no data )", "No projects yet",
"Create one under projects/{name}/ with project.md and reindex");
ImGui::EndChild();
code_block(
"if (apps.empty()) {\n"
" empty_state(\"( no data )\", \"No apps yet\",\n"
" \"Click + Add to create one\");\n"
" return;\n"
"}"
);
}
// ---------------------------------------------------------------------------
// page_header
// ---------------------------------------------------------------------------
void demo_page_header() {
demo_header("page_header", "v1.0.0",
"Header de pagina con titulo, subtitulo opcional y separador final. "
"Patron begin/end permite insertar acciones entre titulo y separador.");
page_header_begin("Dashboard", "13 apps, 3 projects, 2 analyses");
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 140.0f);
toolbar_begin();
button("Reload", V::Subtle); ImGui::SameLine();
button("+ Add", V::Secondary);
toolbar_end();
page_header_end();
code_block(
"page_header_begin(\"Dashboard\", subtitle);\n"
"ImGui::SameLine(ImGui::GetContentRegionAvail().x - 140);\n"
"toolbar_begin();\n"
" button(\"Reload\", Subtle);\n"
"toolbar_end();\n"
"page_header_end();"
);
}
// ---------------------------------------------------------------------------
// dashboard_panel
// ---------------------------------------------------------------------------
void demo_dashboard_panel() {
demo_header("dashboard_panel", "v1.0.0",
"Contenedor tipo panel con titulo, bordes redondeados, bg surface. "
"Auto-resize-Y segun contenido. Usa min_width/min_height como piso.");
if (dashboard_panel_begin("Revenue", 0, 120.0f)) {
ImGui::Text("Some panel content goes here.");
ImGui::Text("Anything drawn inside lives in the child window.");
}
dashboard_panel_end();
code_block(
"if (dashboard_panel_begin(\"Revenue\", 0, 120.0f)) {\n"
" ImGui::Text(\"content\");\n"
"}\n"
"dashboard_panel_end();"
);
}
} // namespace gallery
+123
View File
@@ -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
+204
View File
@@ -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
+211
View File
@@ -0,0 +1,211 @@
#include "demos.h"
#include "demo.h"
#include "viz/bar_chart.h"
#include "viz/pie_chart.h"
#include "viz/line_plot.h"
#include "viz/scatter_plot.h"
#include "viz/histogram.h"
#include "viz/sparkline.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cmath>
#include <vector>
namespace gallery {
// ---------------------------------------------------------------------------
// bar_chart
// ---------------------------------------------------------------------------
void demo_bar_chart() {
demo_header("bar_chart", "v1.2.0",
"Barras verticales con ejes pineados, tooltip al hover y auto-rotacion 45 grados "
"de labels cuando no caben horizontalmente.");
section("Labels que caben horizontalmente");
{
const char* langs[] = {"go", "py", "ts", "sh", "cpp"};
float values[] = {412.0f, 187.0f, 94.0f, 63.0f, 36.0f};
bar_chart("##bar_short", langs, values, 5, 0.67f, 200.0f);
}
section("Labels largos que obligan a rotar");
{
const char* domains[] = {
"core", "infrastructure", "finance", "datascience",
"cybersecurity", "notebook", "browser"
};
float values[] = {412, 187, 94, 63, 42, 38, 22};
bar_chart("##bar_long", domains, values, 7, 0.67f, 240.0f);
}
code_block(
"const char* labels[] = {\"go\",\"py\",\"ts\",\"sh\",\"cpp\"};\n"
"float values[] = {412,187,94,63,36};\n"
"bar_chart(\"##lang\", labels, values, 5); // h=200 default\n"
"bar_chart(\"##lang\", labels, values, 5, 0.8f, 300); // bar_w + altura"
);
}
// ---------------------------------------------------------------------------
// pie_chart
// ---------------------------------------------------------------------------
void demo_pie_chart() {
demo_header("pie_chart", "v1.1.0",
"Pie/donut con aspect 1:1, ejes pineados y tooltip por slice con "
"valor absoluto + porcentaje.");
if (ImGui::BeginTable("##pie_grid", 2, ImGuiTableFlags_SizingStretchSame)) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
{
const char* labels[] = {"Pure", "Impure"};
float values[] = {412.0f, 278.0f};
variant_label("Pie (radius auto)");
pie_chart("##pie_auto", labels, values, 2, 0.0f, 260.0f);
}
ImGui::TableSetColumnIndex(1);
{
const char* labels[] = {"function", "pipeline", "component"};
float values[] = {618.0f, 42.0f, 230.0f};
variant_label("Donut (radius = -0.45)");
pie_chart("##pie_donut", labels, values, 3, -0.45f, 260.0f);
}
ImGui::EndTable();
}
code_block(
"const char* labels[] = {\"Pure\",\"Impure\"};\n"
"float values[] = {412, 278};\n"
"pie_chart(\"##p\", labels, values, 2); // pie auto\n"
"pie_chart(\"##p\", labels, values, 2, -0.45f, 260); // donut"
);
}
// ---------------------------------------------------------------------------
// line_plot
// ---------------------------------------------------------------------------
void demo_line_plot() {
demo_header("line_plot", "v1.1.0",
"Line plot 2D con limites de ejes calculados de min/max y pineados. "
"Sin auto-fit animado, sin pan/zoom.");
constexpr int N = 100;
static float xs[N], ys[N];
static bool init = false;
if (!init) {
for (int i = 0; i < N; i++) {
xs[i] = static_cast<float>(i) * 0.1f;
ys[i] = std::sin(xs[i]) + 0.3f * std::sin(xs[i] * 3.5f);
}
init = true;
}
line_plot("##line", xs, ys, N, 240.0f);
code_block(
"line_plot(\"##series\", xs, ys, count); // h=200 default\n"
"line_plot(\"##series\", xs, ys, count, 300.0f); // custom height"
);
}
// ---------------------------------------------------------------------------
// scatter_plot
// ---------------------------------------------------------------------------
void demo_scatter_plot() {
demo_header("scatter_plot", "v1.1.0",
"Puntos dispersos con ejes pineados (5% headroom). Sin interaccion.");
constexpr int N = 120;
static float xs[N], ys[N];
static bool init = false;
if (!init) {
unsigned seed = 1234;
auto rnd = [&]() {
seed = seed * 1103515245u + 12345u;
return static_cast<float>((seed >> 16) & 0x7fff) / 32768.0f;
};
for (int i = 0; i < N; i++) {
xs[i] = rnd() * 10.0f;
ys[i] = 0.5f * xs[i] + rnd() * 3.0f;
}
init = true;
}
scatter_plot("##sc", xs, ys, N, 240.0f);
code_block(
"scatter_plot(\"##xy\", xs, ys, count, 240.0f);"
);
}
// ---------------------------------------------------------------------------
// histogram
// ---------------------------------------------------------------------------
void demo_histogram() {
demo_header("histogram", "v1.1.0",
"Histograma con bins automaticos (Sturges) o manuales. Usa AutoFit "
"para los bins + Lock para bloquear pan/zoom.");
constexpr int N = 300;
static float vals[N];
static bool init = false;
if (!init) {
unsigned seed = 42;
auto rnd = [&]() {
seed = seed * 1103515245u + 12345u;
return static_cast<float>((seed >> 16) & 0x7fff) / 32768.0f;
};
// Aproximacion de distribucion normal via box-muller simplificado
for (int i = 0; i < N; i++) {
float u1 = rnd() + 1e-6f;
float u2 = rnd();
vals[i] = std::sqrt(-2.0f * std::log(u1))
* std::cos(2.0f * 3.14159f * u2);
}
init = true;
}
histogram("##hist", vals, N, -1, 240.0f);
code_block(
"histogram(\"##h\", values, count); // bins=Sturges\n"
"histogram(\"##h\", values, count, 30, 300.0f); // 30 bins, h=300"
);
}
// ---------------------------------------------------------------------------
// sparkline
// ---------------------------------------------------------------------------
void demo_sparkline() {
demo_header("sparkline", "v1.0.0",
"Mini grafico de lineas inline (rellenado con alpha + linea). "
"Pensado para tablas, KPI cards, headers.");
float up[] = {10, 12, 11, 15, 18, 17, 20};
float down[] = {30, 28, 29, 25, 22, 24, 20};
float flat[] = {10, 10, 10, 10, 10, 10, 10};
ImGui::Text("Trending up "); ImGui::SameLine();
sparkline("##up", up, 7, ImVec4(0.35f, 0.85f, 0.45f, 1.0f), 140.0f, 22.0f);
ImGui::Text("Trending down"); ImGui::SameLine();
sparkline("##down", down, 7, ImVec4(0.90f, 0.30f, 0.30f, 1.0f), 140.0f, 22.0f);
ImGui::Text("Flat "); ImGui::SameLine();
sparkline("##flat", flat, 7, ImVec4(0.55f, 0.55f, 0.55f, 1.0f), 140.0f, 22.0f);
code_block(
"float history[] = {10,12,11,15,18,17,20};\n"
"sparkline(\"##rev\", history, 7, /*color=*/{0.35,0.85,0.45,1}, 140, 22);"
);
}
} // namespace gallery
+159
View File
@@ -0,0 +1,159 @@
// primitives_gallery — catalogo visual interactivo de los primitivos UI
// del registry (cpp/functions/core y cpp/functions/viz).
//
// 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
);
}
+14
View File
@@ -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
)
+64
View File
@@ -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;
}
+283
View File
@@ -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
+41
View File
@@ -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
+93
View File
@@ -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.
+68
View File
@@ -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
+50
View File
@@ -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
+101
View File
@@ -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.
+48
View File
@@ -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
```
File diff suppressed because it is too large Load Diff
+389
View File
@@ -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;
};