feat(cpp/core): primitivas UI estilo Mantine

Anade 9 primitivas reutilizables al registry C++ que replican el comportamiento
de los componentes correspondientes de @fn_library / Mantine v9, todas
estilizadas con tokens_cpp_core (colores Mantine dark + indigo):

- button_cpp_core         (component, pure)  variantes primary/secondary/subtle/danger + sm/md/lg
- icon_button_cpp_core    (component, pure)  cuadrado 28x28 con glyph centrado + tooltip
- toolbar_cpp_core        (component, pure)  grupo horizontal de acciones con separadores
- modal_dialog_cpp_core   (component, pure)  popup modal centrada + close con Escape
- text_input_cpp_core     (component, impure) InputText con label muted + placeholder
- select_cpp_core         (component, impure) dropdown con label + opcion '(none)' opcional
- toast_cpp_core          (component, impure) notificaciones efimeras + inbox con badge
- tree_view_cpp_core      (component, impure) jerarquia low-level con tree_node_clicked helper
- process_runner_cpp_core (component, impure) tarea en std::thread + spinner inline

Cada primitiva tiene su .md con frontmatter completo (params/output) y se
indexa via fn index. Son la base del primitives_gallery y de cualquier
app fn_ui futura.
This commit is contained in:
2026-04-25 21:25:39 +02:00
parent 79591daef2
commit dff0d735c1
27 changed files with 1601 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
#include "core/button.h"
#include "core/tokens.h"
#include <imgui.h>
namespace fn_ui {
namespace {
struct VariantColors {
ImVec4 bg;
ImVec4 bg_hover;
ImVec4 bg_active;
ImVec4 text;
ImVec4 border;
float border_size; // 0 = sin borde
};
VariantColors colors_for(ButtonVariant v) {
using namespace fn_tokens::colors;
switch (v) {
case ButtonVariant::Primary:
return {primary, primary_hover, primary, text, primary, 0.0f};
case ButtonVariant::Secondary:
return {surface, surface_hover, surface, text, border, 1.0f};
case ButtonVariant::Subtle:
return {ImVec4(0,0,0,0), surface_hover, surface, primary, ImVec4(0,0,0,0),0.0f};
case ButtonVariant::Danger:
return {error, error, error, text, error, 0.0f};
}
return {surface, surface_hover, surface, text, border, 1.0f};
}
ImVec2 padding_for(ButtonSize s) {
using namespace fn_tokens::spacing;
switch (s) {
case ButtonSize::Sm: return ImVec2(md, xs + 2.0f);
case ButtonSize::Md: return ImVec2(lg, sm);
case ButtonSize::Lg: return ImVec2(xl, md);
}
return ImVec2(lg, sm);
}
float font_scale_for(ButtonSize s) {
return (s == ButtonSize::Lg) ? 1.1f : 1.0f;
}
} // namespace
bool button(const char* label, ButtonVariant variant, ButtonSize size) {
using namespace fn_tokens;
const VariantColors c = colors_for(variant);
const ImVec2 pad = padding_for(size);
const float fs = font_scale_for(size);
ImGui::PushStyleColor(ImGuiCol_Button, c.bg);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, c.bg_hover);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, c.bg_active);
ImGui::PushStyleColor(ImGuiCol_Text, c.text);
ImGui::PushStyleColor(ImGuiCol_Border, c.border);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, radius::sm);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, c.border_size);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, pad);
if (fs != 1.0f) ImGui::SetWindowFontScale(fs);
const bool clicked = ImGui::Button(label);
if (fs != 1.0f) ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleVar(3);
ImGui::PopStyleColor(5);
return clicked;
}
} // namespace fn_ui
+30
View File
@@ -0,0 +1,30 @@
#pragma once
// Button con variantes (primary/secondary/subtle/danger) y tamanos (sm/md/lg)
// usando fn_tokens. Replica el <Button> de Mantine v9 / @fn_library.
//
// Uso:
// if (fn_ui::button("Save", fn_ui::ButtonVariant::Primary)) { ... }
// if (fn_ui::button("Cancel", fn_ui::ButtonVariant::Subtle)) { ... }
namespace fn_ui {
enum class ButtonVariant {
Primary, // indigo bg, blanco text (Mantine filled)
Secondary, // surface bg, border, text normal (default)
Subtle, // transparente, text primary, hover surface
Danger, // error bg, blanco text
};
enum class ButtonSize {
Sm, // altura ~24, padding 6x12, font default
Md, // altura ~32, padding 8x16, font default (default)
Lg, // altura ~40, padding 10x20, font x1.1
};
// Renderiza el boton y devuelve true el frame en que se hace click.
bool button(const char* label,
ButtonVariant variant = ButtonVariant::Secondary,
ButtonSize size = ButtonSize::Md);
} // namespace fn_ui
+60
View File
@@ -0,0 +1,60 @@
---
name: button
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "bool fn_ui::button(const char* label, fn_ui::ButtonVariant variant = Secondary, fn_ui::ButtonSize size = Md)"
description: "Boton ImGui con variantes (primary/secondary/subtle/danger) y tamanos (sm/md/lg) usando fn_tokens. Replica Mantine Button / fn_library Button."
tags: [imgui, button, ui, form, tokens]
uses_functions: ["tokens_cpp_core"]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/button.cpp"
framework: imgui
params:
- name: label
desc: "Texto visible del boton (tambien se usa como id ImGui; prefijar con ## para solo-id)"
- name: variant
desc: "ButtonVariant: Primary (indigo), Secondary (surface+border, default), Subtle (transparente), Danger (rojo)"
- name: size
desc: "ButtonSize: Sm (compacto), Md (default), Lg (grande, font x1.1)"
output: "true el frame en que se hace click, false en caso contrario"
---
# button
Boton con variantes semanticas y tamanos fijos. Usa tokens (`primary`, `surface`, `error`, `border`, `text`) y padding/radius consistentes — mismo estilo visual en cualquier app C++ que lo use.
## Variantes
| Variant | Uso | Colores |
|---------|-----|---------|
| `Primary` | Accion principal (Save, Submit) | `colors::primary` + hover |
| `Secondary` | Accion neutra (Cancel) | `surface` + `border` |
| `Subtle` | Accion discreta (ver mas) | transparente + hover surface |
| `Danger` | Accion destructiva (Delete) | `colors::error` |
## Ejemplo
```cpp
#include "core/button.h"
using fn_ui::button;
using V = fn_ui::ButtonVariant;
if (button("Reindex", V::Primary)) { ... }
if (button("Cancel", V::Subtle)) { ... }
if (button("Delete", V::Danger)) { ... }
```
## Notas
- No gestiona layout: usar `ImGui::SameLine()` o `toolbar` para alinear varios.
- Para botones solo-icono sin texto, usar `icon_button`.
+35
View File
@@ -0,0 +1,35 @@
#include "core/icon_button.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cstdio>
namespace fn_ui {
bool icon_button(const char* id, const char* glyph, const char* tooltip) {
using namespace fn_tokens;
// Estilo: subtle por defecto (transparente, hover surface_hover).
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::surface_hover);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, colors::surface);
ImGui::PushStyleColor(ImGuiCol_Text, colors::text);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, radius::sm);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(spacing::xs, spacing::xs));
// Componer label_visible##id para que el label sea solo el glyph pero el
// id del widget sea el parametro `id` (debe ser unico).
char label[128];
std::snprintf(label, sizeof(label), "%s%s", glyph ? glyph : "?", id);
const bool clicked = ImGui::Button(label, ImVec2(28.0f, 28.0f));
if (tooltip && *tooltip && ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", tooltip);
}
ImGui::PopStyleVar(2);
ImGui::PopStyleColor(4);
return clicked;
}
} // namespace fn_ui
+20
View File
@@ -0,0 +1,20 @@
#pragma once
// Boton cuadrado solo con un glyph (UTF-8) o con un dibujo custom.
// Compacto, apto para toolbars.
//
// Uso:
// if (fn_ui::icon_button("##reload", "\xe2\x86\xbb")) { ... } // UTF-8 ↻
namespace fn_ui {
// Renderiza un boton cuadrado de 28x28px con el glyph centrado.
// `id` debe ser unico (prefijar con "##" si no quieres texto visible).
// `glyph` es la cadena UTF-8 a renderizar (el sistema de fuentes de ImGui
// debe tener el codepoint cargado).
// `tooltip` opcional: si no es null, muestra tooltip al pasar el mouse.
// Devuelve true el frame del click.
bool icon_button(const char* id, const char* glyph,
const char* tooltip = nullptr);
} // namespace fn_ui
+74
View File
@@ -0,0 +1,74 @@
---
name: icon_button
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "bool fn_ui::icon_button(const char* id, const char* glyph, const char* tooltip = nullptr)"
description: "Boton cuadrado 28x28 con glyph UTF-8 centrado y tooltip opcional. Estilo subtle (transparente con hover). Apto para toolbars."
tags: [imgui, button, icon, ui, toolbar, tokens]
uses_functions: ["tokens_cpp_core"]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/icon_button.cpp"
framework: imgui
params:
- name: id
desc: "Id unico ImGui (prefijar con ## si no quieres texto visible ademas del glyph)"
- name: glyph
desc: "Cadena UTF-8 del icono (e.g. \\xe2\\x86\\xbb para reload). Debe existir en el atlas de la fuente activa"
- name: tooltip
desc: "Texto del tooltip al hover (opcional, null para omitir)"
output: "true el frame en que se hace click"
---
# icon_button
Boton cuadrado de 28x28px con un glyph UTF-8 centrado. Estilo subtle (sin fondo hasta hover) pensado para toolbars compactas.
## Glyphs utiles
| UTF-8 (`\x..`) | Unicode | Visual | Uso |
|---|---|---|---|
| `\xe2\x86\xbb` | U+21BB | ↻ | Reload |
| `\xe2\x9c\x8f` | U+270F | ✏ | Edit |
| `\xef\x80\x8c` | U+F00C | ✓ | Check (FontAwesome) |
| `\xe2\x9e\x95` | U+2795 | | Add |
| `\xe2\x9d\x8c` | U+274C | ❌ | Delete |
| `\xe2\x96\xbc` | U+25BC | ▼ | Dropdown |
| `\xe2\x9a\x99` | U+2699 | ⚙ | Settings |
Asegurate de que el glyph este en el rango de la fuente (anadir a `ImFontAtlas::GetGlyphRangesDefault` + rangos simbolicos si hace falta).
## Ejemplo
```cpp
if (fn_ui::icon_button("##reload", "\xe2\x86\xbb", "Reindex")) reindex();
if (fn_ui::icon_button("##add", "\xe2\x9e\x95", "Add...")) open_menu();
```
## Notas — Tabler Icons (preferido)
La tabla de glyphs UTF-8 de arriba es **historica**. A partir de la integracion de `icon_font_cpp_core` + `core/icons_tabler.h`, el atlas activo en cualquier app que use `fn::run_app` es Tabler v3.41.1 (5093 glyphs en U+E000..U+FCFF). Los hex UTF-8 inline (`\xe2\x9a\x99`, etc.) **NO renderizan** porque la fuente default de ImGui no incluye esos codepoints.
Patron correcto:
```cpp
#include "core/icon_button.h"
#include "core/icons_tabler.h"
if (icon_button("##reload", TI_REFRESH, "Reindex")) reindex();
if (icon_button("##add", TI_PLUS, "Add...")) open_menu();
if (icon_button("##settings", TI_SETTINGS, "Settings")) open_settings();
if (icon_button("##save", TI_DEVICE_FLOPPY, "Save")) save();
if (icon_button("##delete", TI_TRASH, "Delete")) confirm();
```
`icon_button` no cambia — sigue aceptando `const char*` en `glyph`. Lo unico que cambia es **donde** sacas la cadena: `TI_*` macro, no UTF-8 hex inline. Ver `cpp/DESIGN_SYSTEM.md` seccion 11 para la regla completa y anti-patrones.
+52
View File
@@ -0,0 +1,52 @@
#include "core/modal_dialog.h"
#include "core/tokens.h"
#include <imgui.h>
namespace fn_ui {
namespace {
bool g_modal_open_frame = false; // se setea en begin, se consulta en end
}
bool modal_dialog_begin(const char* title, bool* open, ImVec2 size) {
using namespace fn_tokens;
if (open && *open) {
ImGui::OpenPopup(title);
// Solo llamamos OpenPopup la primera vez que pasa a true — pero es
// idempotente si ya esta abierto, asi que no hace falta edge detect.
}
// Centrar
const ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (size.x > 0.0f || size.y > 0.0f) {
ImGui::SetNextWindowSize(size, ImGuiCond_Appearing);
}
// Estilo acorde con las cards
ImGui::PushStyleColor(ImGuiCol_PopupBg, colors::surface);
ImGui::PushStyleColor(ImGuiCol_Border, colors::border);
ImGui::PushStyleColor(ImGuiCol_TitleBg, colors::surface);
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, colors::surface);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, radius::md);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::lg, spacing::lg));
bool ret = ImGui::BeginPopupModal(title, open,
ImGuiWindowFlags_NoSavedSettings
| ImGuiWindowFlags_AlwaysAutoResize);
g_modal_open_frame = ret;
return ret;
}
void modal_dialog_end() {
if (g_modal_open_frame) {
ImGui::EndPopup();
}
ImGui::PopStyleVar(3);
ImGui::PopStyleColor(4);
g_modal_open_frame = false;
}
} // namespace fn_ui
+30
View File
@@ -0,0 +1,30 @@
#pragma once
// Modal dialog centrado con titulo, contenido custom del caller y cierre
// al pulsar Escape / click fuera. Patron show-flag + begin/end.
//
// Uso tipico:
// static bool show = false;
// if (button("Open")) show = true;
//
// if (fn_ui::modal_dialog_begin("Add App", &show, ImVec2(420, 0))) {
// text_input("Name", buf, sizeof(buf));
// ...
// if (button("Cancel")) show = false;
// ImGui::SameLine();
// if (button("Create", Primary)) { create(); show = false; }
// }
// fn_ui::modal_dialog_end(); // llamar siempre, aunque begin devuelva false
#include "imgui.h"
namespace fn_ui {
// Abre (si `*open` es true) una popup modal centrada con el titulo dado.
// `size` = (0,0) autosize. Devuelve true si el modal esta visible — entonces
// el caller renderiza el contenido; al terminar llama siempre a modal_dialog_end.
// `open` puede ser null si se gestiona el close desde dentro con ImGui::CloseCurrentPopup.
bool modal_dialog_begin(const char* title, bool* open, ImVec2 size = ImVec2(0, 0));
void modal_dialog_end();
} // namespace fn_ui
+57
View File
@@ -0,0 +1,57 @@
---
name: modal_dialog
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "bool fn_ui::modal_dialog_begin(const char* title, bool* open, ImVec2 size = ImVec2(0, 0)); void fn_ui::modal_dialog_end()"
description: "Popup modal centrada con titulo, estilo surface+border acorde con tokens, close con Escape o click en X. Patron begin/end."
tags: [imgui, modal, dialog, ui, form, tokens]
uses_functions: ["tokens_cpp_core"]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/modal_dialog.cpp"
framework: imgui
params:
- name: title
desc: "Titulo visible en la cabecera del modal (tambien se usa como popup id de ImGui)"
- name: open
desc: "Puntero a bool que controla apertura (true abre, false cierra). Puede ser null si se gestiona el close desde dentro con ImGui::CloseCurrentPopup"
- name: size
desc: "Tamano fijo (x,y). (0,0) = autosize segun contenido"
output: "modal_dialog_begin devuelve true si el modal esta visible este frame; modal_dialog_end cierra siempre (llamar incondicionalmente)"
---
# modal_dialog
Popup modal centrada en el viewport con estilo consistente de tokens (`surface` bg, `border` 1px, `radius::md`, padding `lg`).
## Patron
```cpp
static bool show_add = false;
if (fn_ui::button("Add", ButtonVariant::Primary)) show_add = true;
if (fn_ui::modal_dialog_begin("Add App", &show_add, ImVec2(420, 0))) {
static char name[128] = {};
fn_ui::text_input("Name", name, sizeof(name));
// ...
if (fn_ui::button("Cancel", ButtonVariant::Subtle)) show_add = false;
ImGui::SameLine();
if (fn_ui::button("Create", ButtonVariant::Primary)) { do_create(); show_add = false; }
}
fn_ui::modal_dialog_end();
```
## Notas
- `modal_dialog_end` debe llamarse SIEMPRE, aunque `begin` devuelva false — hace pop de styles aunque no haya `EndPopup`.
- `AlwaysAutoResize` hace crecer el modal con el contenido hasta `size` si se pasa.
- Para forms multi-campo preferir `size.x > 0` fijo y `size.y = 0` (alto autosize).
+99
View File
@@ -0,0 +1,99 @@
#include "core/process_runner.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cmath>
namespace fn_ui {
ProcessRunner::ProcessRunner() = default;
ProcessRunner::~ProcessRunner() {
if (th_.joinable()) th_.join();
}
bool ProcessRunner::is_busy() const {
return state_.load() == static_cast<int>(RunnerState::Running);
}
RunnerState ProcessRunner::state() const {
return static_cast<RunnerState>(state_.load());
}
std::string ProcessRunner::message() const {
std::lock_guard<std::mutex> lk(mu_);
return message_;
}
void ProcessRunner::reset() {
if (is_busy()) return;
state_.store(static_cast<int>(RunnerState::Idle));
std::lock_guard<std::mutex> lk(mu_);
message_.clear();
}
void runner_trigger(ProcessRunner& r,
std::function<bool(std::string&)> task) {
if (r.is_busy()) return;
if (r.th_.joinable()) r.th_.join();
r.state_.store(static_cast<int>(RunnerState::Running));
{
std::lock_guard<std::mutex> lk(r.mu_);
r.message_.clear();
}
r.th_ = std::thread([&r, task = std::move(task)]() {
std::string out;
bool ok = false;
try {
ok = task(out);
} catch (...) {
ok = false;
out = "exception";
}
{
std::lock_guard<std::mutex> lk(r.mu_);
r.message_ = std::move(out);
}
r.state_.store(static_cast<int>(ok ? RunnerState::Success : RunnerState::Error));
});
}
void runner_status(const ProcessRunner& r, const char* running_label) {
using namespace fn_tokens;
const RunnerState s = r.state();
if (s == RunnerState::Idle) return;
if (s == RunnerState::Running) {
// Spinner manual: un punto que rota
const float t = static_cast<float>(ImGui::GetTime());
const float radius = 6.0f;
const ImVec2 cur = ImGui::GetCursorScreenPos();
const ImVec2 center(cur.x + radius + 2.0f,
cur.y + ImGui::GetFrameHeight() * 0.5f);
ImDrawList* dl = ImGui::GetWindowDrawList();
const ImU32 col_fg = ImGui::ColorConvertFloat4ToU32(colors::primary);
const ImU32 col_bg = ImGui::ColorConvertFloat4ToU32(colors::border);
dl->AddCircle(center, radius, col_bg, 16, 2.0f);
const float a = t * 3.0f;
const ImVec2 dot(center.x + std::cos(a) * radius,
center.y + std::sin(a) * radius);
dl->AddCircleFilled(dot, 2.5f, col_fg);
ImGui::Dummy(ImVec2(radius * 2.0f + 8.0f, ImGui::GetFrameHeight()));
ImGui::SameLine();
ImGui::TextUnformatted(running_label ? running_label : "working...");
return;
}
const ImVec4 col = (s == RunnerState::Success)
? colors::success : colors::error;
ImGui::PushStyleColor(ImGuiCol_Text, col);
const char* glyph = (s == RunnerState::Success)
? "\xe2\x9c\x93 " : "\xe2\x9c\x97 "; // ✓ / ✗
ImGui::TextUnformatted(glyph);
ImGui::SameLine();
ImGui::TextWrapped("%s", r.message().c_str());
ImGui::PopStyleColor();
}
} // namespace fn_ui
+65
View File
@@ -0,0 +1,65 @@
#pragma once
// Ejecuta una tarea en un thread en background y expone su estado para que
// la UI pueda mostrar spinner + resultado. La tarea recibe un out-string
// por referencia y devuelve true si termino con exito.
//
// Uso:
// static fn_ui::ProcessRunner reindex_runner;
//
// if (button("Reindex", Primary) && !reindex_runner.is_busy()) {
// fn_ui::runner_trigger(reindex_runner, [](std::string& out) -> bool {
// return http_post("http://127.0.0.1:8484/api/reindex", "", &out);
// });
// }
// fn_ui::runner_status(reindex_runner, "Reindexing...");
#include <atomic>
#include <functional>
#include <mutex>
#include <string>
#include <thread>
namespace fn_ui {
enum class RunnerState : int {
Idle = 0,
Running = 1,
Success = 2,
Error = 3,
};
class ProcessRunner {
public:
ProcessRunner();
~ProcessRunner();
ProcessRunner(const ProcessRunner&) = delete;
ProcessRunner& operator=(const ProcessRunner&) = delete;
bool is_busy() const;
RunnerState state() const;
std::string message() const; // thread-safe snapshot del ultimo message
void reset(); // vuelve a Idle (solo si no esta Running)
private:
friend void runner_trigger(ProcessRunner&, std::function<bool(std::string&)>);
friend void runner_status(const ProcessRunner&, const char*);
std::atomic<int> state_{0};
mutable std::mutex mu_;
std::string message_;
std::thread th_;
};
// Lanza `task` en background; al terminar, guarda el out-string y marca
// Success/Error. Si el runner ya esta corriendo, la llamada se ignora.
void runner_trigger(ProcessRunner& r,
std::function<bool(std::string&)> task);
// Renderiza inline un spinner ImGui + texto de estado segun runner.state.
// `running_label` es el texto mostrado mientras corre (e.g. "Reindexing...").
// En Success muestra el message completo en color success; en Error idem en
// error.
void runner_status(const ProcessRunner& r, const char* running_label);
} // namespace fn_ui
+61
View File
@@ -0,0 +1,61 @@
---
name: process_runner
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: impure
signature: "class fn_ui::ProcessRunner { is_busy(); state(); message(); reset(); }; void runner_trigger(ProcessRunner&, std::function<bool(std::string&)>); void runner_status(const ProcessRunner&, const char* label)"
description: "Ejecuta una tarea en std::thread en background y expone estado thread-safe (idle/running/success/error). Incluye widget inline con spinner + resultado."
tags: [imgui, ui, async, thread, runner, tokens]
uses_functions: ["tokens_cpp_core"]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui, thread, mutex, atomic]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/process_runner.cpp"
framework: imgui
params: []
output: "ProcessRunner: objeto que encapsula un thread + estado. runner_trigger lanza la tarea. runner_status renderiza spinner + mensaje segun state."
---
# process_runner
Wrapper para ejecutar operaciones lentas (HTTP requests, subprocesos) sin bloquear el UI thread. Threading simple con `std::thread` y estado `std::atomic<int>` + mensaje protegido por `std::mutex`.
## Uso tipico — reindex desde un boton
```cpp
#include "core/process_runner.h"
#include "core/button.h"
static fn_ui::ProcessRunner reindex_runner;
// En el render:
if (fn_ui::button("Reindex", ButtonVariant::Primary) && !reindex_runner.is_busy()) {
fn_ui::runner_trigger(reindex_runner, [](std::string& out) -> bool {
// Esto corre en el thread en background:
return http_post("http://127.0.0.1:8484/api/reindex", "", &out);
});
}
ImGui::SameLine();
fn_ui::runner_status(reindex_runner, "Reindexing...");
```
Mientras corre muestra un circulo con un punto rotando + "Reindexing...". Al terminar muestra un ✓ o ✗ con el `message` devuelto por la tarea.
## Thread safety
- `state()` es lock-free (atomic load).
- `message()` toma lock (copia string).
- La tarea recibe un `std::string&` que SE MUTA desde el thread; el lock lo toma internamente al final para guardar el resultado.
- `runner_trigger` ignora la llamada si el runner ya esta Running (no permite concurrencia sobre el mismo runner — usar runners separados para operaciones paralelas).
## Notas
- El destructor hace `join()` del thread, asi que si la tarea tarda, cerrar la app espera. HTTP calls con timeout razonable hacen esto seguro.
- `reset()` pasa a Idle si no esta Running — util despues de mostrar el resultado para limpiar el UI.
+62
View File
@@ -0,0 +1,62 @@
#include "core/select.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cstdio>
namespace fn_ui {
bool select(const char* label, int* selected_idx,
const char* const* options, int count,
bool allow_none) {
using namespace fn_tokens;
if (!selected_idx) return false;
// Label muted
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::TextUnformatted(label);
ImGui::PopStyleColor();
// Combo con estilo tokens
ImGui::PushStyleColor(ImGuiCol_FrameBg, colors::bg);
ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, colors::surface_hover);
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, colors::surface);
ImGui::PushStyleColor(ImGuiCol_Border, colors::border);
ImGui::PushStyleColor(ImGuiCol_PopupBg, colors::surface);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::surface_hover);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, radius::sm);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(spacing::sm, spacing::xs + 2.0f));
char id[160];
std::snprintf(id, sizeof(id), "##%s", label);
ImGui::SetNextItemWidth(-FLT_MIN);
const char* preview = "(none)";
if (*selected_idx >= 0 && *selected_idx < count) preview = options[*selected_idx];
bool changed = false;
if (ImGui::BeginCombo(id, preview)) {
if (allow_none) {
bool is_sel = (*selected_idx == -1);
if (ImGui::Selectable("(none)", is_sel)) {
if (*selected_idx != -1) { *selected_idx = -1; changed = true; }
}
if (is_sel) ImGui::SetItemDefaultFocus();
}
for (int i = 0; i < count; i++) {
bool is_sel = (*selected_idx == i);
if (ImGui::Selectable(options[i], is_sel)) {
if (*selected_idx != i) { *selected_idx = i; changed = true; }
}
if (is_sel) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
ImGui::PopStyleVar(3);
ImGui::PopStyleColor(7);
return changed;
}
} // namespace fn_ui
+21
View File
@@ -0,0 +1,21 @@
#pragma once
// Select/dropdown con label arriba, lista de opciones y opcional "None".
// Equivalente simple de <Select> de Mantine / fn_library.
//
// Uso:
// static int idx = 0;
// const char* langs[] = {"go", "py", "ts", "sh", "cpp"};
// fn_ui::select("Language", &idx, langs, 5);
namespace fn_ui {
// Renderiza label muted arriba + combo abajo. `selected_idx` se muta cuando
// el usuario elige una opcion. Devuelve true si cambio en este frame.
// `options[i]` son los strings visibles; el indice seleccionado va de 0 a count-1.
// Si allow_none es true, aparece una entrada "(none)" con indice -1.
bool select(const char* label, int* selected_idx,
const char* const* options, int count,
bool allow_none = false);
} // namespace fn_ui
+51
View File
@@ -0,0 +1,51 @@
---
name: select
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: impure
signature: "bool fn_ui::select(const char* label, int* selected_idx, const char* const* options, int count, bool allow_none = false)"
description: "Select/dropdown ImGui con label muted, estilo surface+border y opcion '(none)' opcional"
tags: [imgui, select, dropdown, form, ui, tokens]
uses_functions: ["tokens_cpp_core"]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/select.cpp"
framework: imgui
params:
- name: label
desc: "Label mostrado arriba, tambien usado como id (prefijado con ##)"
- name: selected_idx
desc: "Puntero a int con el indice seleccionado. -1 = ninguno (solo si allow_none)"
- name: options
desc: "Array de C-strings con las opciones visibles"
- name: count
desc: "Numero de opciones"
- name: allow_none
desc: "Si true, aparece '(none)' como opcion inicial con indice -1"
output: "true el frame en que la seleccion cambio"
---
# select
Label muted + combo estilizado con tokens. Apto para campos de formulario (lang, domain, project parent).
## Ejemplo
```cpp
static int lang_idx = 0;
const char* langs[] = {"go", "py", "ts", "sh", "cpp"};
fn_ui::select("Language", &lang_idx, langs, 5);
// Con opcion "ninguno"
static int project_idx = -1;
fn_ui::select("Project", &project_idx, project_names.data(),
(int)project_names.size(), true);
```
+52
View File
@@ -0,0 +1,52 @@
#include "core/text_input.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cstdio>
namespace fn_ui {
bool text_input(const char* label, char* buf, size_t buf_size,
const char* placeholder) {
using namespace fn_tokens;
// Label muted arriba
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::TextUnformatted(label);
ImGui::PopStyleColor();
// Input con estilo tokens
ImGui::PushStyleColor(ImGuiCol_FrameBg, colors::bg);
ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, colors::surface_hover);
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, colors::surface);
ImGui::PushStyleColor(ImGuiCol_Border, colors::border);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, radius::sm);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(spacing::sm, spacing::xs + 2.0f));
char id[160];
std::snprintf(id, sizeof(id), "##%s", label);
ImGui::SetNextItemWidth(-FLT_MIN); // full width
ImGuiInputTextFlags flags = ImGuiInputTextFlags_None;
bool changed = false;
if (placeholder && *placeholder && buf[0] == '\0') {
// Mostrar placeholder en text_dim cuando vacio: ImGui no tiene API
// oficial; dibujamos el hint por encima si el buffer esta vacio.
ImVec2 cur = ImGui::GetCursorScreenPos();
changed = ImGui::InputText(id, buf, buf_size, flags);
ImDrawList* dl = ImGui::GetWindowDrawList();
const ImVec4 dim = colors::text_dim;
const ImU32 col = ImGui::ColorConvertFloat4ToU32(dim);
dl->AddText(ImVec2(cur.x + spacing::sm,
cur.y + (spacing::xs + 2.0f)),
col, placeholder);
} else {
changed = ImGui::InputText(id, buf, buf_size, flags);
}
ImGui::PopStyleVar(3);
ImGui::PopStyleColor(4);
return changed;
}
} // namespace fn_ui
+19
View File
@@ -0,0 +1,19 @@
#pragma once
// Input de texto con label arriba y estilo acorde con fn_tokens.
// Equivalente simple de <TextInput> de Mantine / fn_library.
//
// Uso:
// static char name[128] = {};
// fn_ui::text_input("Name", name, sizeof(name), "my-app");
#include <cstddef>
namespace fn_ui {
// Renderiza label muted arriba + input abajo (stacked). Devuelve true si el
// valor cambio este frame. `placeholder` opcional (hint cuando esta vacio).
bool text_input(const char* label, char* buf, size_t buf_size,
const char* placeholder = nullptr);
} // namespace fn_ui
+45
View File
@@ -0,0 +1,45 @@
---
name: text_input
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: impure
signature: "bool fn_ui::text_input(const char* label, char* buf, size_t buf_size, const char* placeholder = nullptr)"
description: "Input de texto ImGui con label muted arriba, estilo surface+border, placeholder opcional. Muta buf."
tags: [imgui, input, form, ui, text, tokens]
uses_functions: ["tokens_cpp_core"]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/text_input.cpp"
framework: imgui
params:
- name: label
desc: "Label mostrado arriba del input, tambien usado como id (prefijado con ##)"
- name: buf
desc: "Buffer char* donde se escribe el valor (se muta in-place)"
- name: buf_size
desc: "Tamano en bytes del buffer"
- name: placeholder
desc: "Hint mostrado en text_dim cuando buf esta vacio (opcional)"
output: "true el frame en que el valor cambio"
---
# text_input
Label muted + input de texto estilizado con tokens. Full-width dentro del contenedor.
## Ejemplo
```cpp
static char name[128] = {};
static char desc[256] = {};
fn_ui::text_input("Name", name, sizeof(name), "my-new-app");
fn_ui::text_input("Description", desc, sizeof(desc));
```
+279
View File
@@ -0,0 +1,279 @@
#include "core/toast.h"
#include "core/tokens.h"
#include <imgui.h>
#include <chrono>
#include <deque>
#include <mutex>
#include <string>
#include <vector>
namespace fn_ui {
namespace {
struct Toast {
ToastKind kind;
std::string text;
std::chrono::steady_clock::time_point created;
};
constexpr float kToastDurationSec = 3.5f;
constexpr float kFadeOutSec = 0.6f;
constexpr float kToastWidth = 320.0f;
constexpr float kToastVerticalGap = 8.0f;
constexpr float kToastMarginX = 16.0f;
constexpr float kToastMarginY = 16.0f;
constexpr int kHistoryMax = 50;
// Cola de toasts activos + historial completo (hasta kHistoryMax).
std::deque<Toast> g_queue;
std::deque<Toast> g_history; // mas reciente al final
int g_unread = 0; // no-leidos desde la ultima vez que se abrio el inbox
std::mutex g_mutex;
ImVec4 color_for(ToastKind k) {
using namespace fn_tokens::colors;
switch (k) {
case ToastKind::Info: return info;
case ToastKind::Success: return success;
case ToastKind::Warning: return warning;
case ToastKind::Error: return error;
}
return info;
}
const char* glyph_for(ToastKind k) {
switch (k) {
case ToastKind::Info: return "\xe2\x84\xb9"; // i
case ToastKind::Success: return "\xe2\x9c\x93"; // check
case ToastKind::Warning: return "\xe2\x9a\xa0"; // warning
case ToastKind::Error: return "\xe2\x9c\x97"; // x
}
return "";
}
std::string format_age(const std::chrono::steady_clock::time_point& t,
const std::chrono::steady_clock::time_point& now) {
auto s = std::chrono::duration_cast<std::chrono::seconds>(now - t).count();
char buf[32];
if (s < 60) std::snprintf(buf, sizeof(buf), "%llds", (long long)s);
else if (s < 3600) std::snprintf(buf, sizeof(buf), "%lldm", (long long)(s / 60));
else std::snprintf(buf, sizeof(buf), "%lldh", (long long)(s / 3600));
return buf;
}
} // namespace
void toast_push(ToastKind kind, const char* text) {
const char* safe = (text && *text) ? text : "(no message)";
std::lock_guard<std::mutex> lk(g_mutex);
auto now = std::chrono::steady_clock::now();
Toast t{kind, std::string(safe), now};
g_queue.push_back(t);
g_history.push_back(t);
while ((int)g_history.size() > kHistoryMax) g_history.pop_front();
g_unread++;
}
void toast_render() {
using namespace fn_tokens;
std::lock_guard<std::mutex> lk(g_mutex);
if (g_queue.empty()) return;
const auto now = std::chrono::steady_clock::now();
while (!g_queue.empty()) {
float age = std::chrono::duration<float>(now - g_queue.front().created).count();
if (age > kToastDurationSec) g_queue.pop_front();
else break;
}
if (g_queue.empty()) return;
const ImGuiViewport* vp = ImGui::GetMainViewport();
float cur_y = vp->WorkPos.y + vp->WorkSize.y - kToastMarginY;
const float x = vp->WorkPos.x + vp->WorkSize.x - kToastMarginX - kToastWidth;
int i = 0;
for (const auto& t : g_queue) {
float age = std::chrono::duration<float>(now - t.created).count();
float alpha = 1.0f;
if (age > kToastDurationSec - kFadeOutSec) {
alpha = (kToastDurationSec - age) / kFadeOutSec;
if (alpha < 0.0f) alpha = 0.0f;
}
ImVec4 accent = color_for(t.kind);
ImVec4 surf = colors::surface;
surf.w *= alpha;
accent.w *= alpha;
ImGui::SetNextWindowPos(ImVec2(x, cur_y), ImGuiCond_Always, ImVec2(0, 1));
ImGui::SetNextWindowSize(ImVec2(kToastWidth, 0));
ImGui::PushStyleColor(ImGuiCol_WindowBg, surf);
ImGui::PushStyleColor(ImGuiCol_Border, accent);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, radius::md);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::md, spacing::sm));
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha);
char id[32];
std::snprintf(id, sizeof(id), "##toast_%d", i);
ImGui::Begin(id, nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize
| ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar
| ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoSavedSettings
| ImGuiWindowFlags_AlwaysAutoResize);
ImGui::PushStyleColor(ImGuiCol_Text, accent);
ImGui::TextUnformatted(glyph_for(t.kind));
ImGui::PopStyleColor();
ImGui::SameLine();
ImGui::TextWrapped("%s", t.text.c_str());
ImVec2 sz = ImGui::GetWindowSize();
ImGui::End();
ImGui::PopStyleVar(4);
ImGui::PopStyleColor(2);
cur_y -= (sz.y + kToastVerticalGap);
i++;
}
}
int toast_unread_count() {
std::lock_guard<std::mutex> lk(g_mutex);
return g_unread;
}
void toast_history_clear() {
std::lock_guard<std::mutex> lk(g_mutex);
g_history.clear();
g_unread = 0;
}
void toast_inbox_button(const char* id) {
using namespace fn_tokens;
// Snapshot tread-safe del estado
int unread = 0;
std::vector<Toast> snapshot;
{
std::lock_guard<std::mutex> lk(g_mutex);
unread = g_unread;
snapshot.assign(g_history.begin(), g_history.end());
}
// Estilo del boton: subtle como icon_button
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::surface_hover);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, colors::surface);
ImGui::PushStyleColor(ImGuiCol_Text, colors::text);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, radius::sm);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(spacing::xs, spacing::xs));
// Label: campana UTF-8 (U+1F514 = bell). Si la fuente no lo tiene,
// fallback a "!".
char label[64];
std::snprintf(label, sizeof(label), "\xf0\x9f\x94\x94%s", id);
ImVec2 btn_pos = ImGui::GetCursorScreenPos();
const bool clicked = ImGui::Button(label, ImVec2(28.0f, 28.0f));
ImVec2 btn_max = ImGui::GetItemRectMax();
ImGui::PopStyleVar(2);
ImGui::PopStyleColor(4);
// Badge: circulo rojo con numero en la esquina superior derecha
if (unread > 0) {
ImDrawList* dl = ImGui::GetWindowDrawList();
const float r = 7.0f;
ImVec2 c(btn_max.x - r + 2.0f, btn_pos.y + r - 2.0f);
dl->AddCircleFilled(c, r, ImGui::ColorConvertFloat4ToU32(colors::error));
char txt[8];
if (unread > 9) std::snprintf(txt, sizeof(txt), "9+");
else std::snprintf(txt, sizeof(txt), "%d", unread);
ImVec2 ts = ImGui::CalcTextSize(txt);
dl->AddText(ImVec2(c.x - ts.x * 0.5f, c.y - ts.y * 0.5f),
IM_COL32_WHITE, txt);
}
char popup_id[64];
std::snprintf(popup_id, sizeof(popup_id), "##inbox_popup%s", id);
// Posicion fija del popup = anclada al boton en el frame del click.
// Acotada al WorkRect del viewport PRIMARIO (IMPORTANTE: con viewports
// activados, una posicion fuera del WorkRect se renderiza en otra
// ventana del OS / otro monitor).
static ImVec2 s_popup_anchor{0, 0};
if (clicked) {
const ImGuiViewport* vp = ImGui::GetMainViewport();
constexpr float popup_w = 360.0f;
constexpr float popup_h = 360.0f;
float x = btn_max.x - popup_w; // alinear lado derecho con el boton
float y = btn_max.y + 4.0f;
// Clamp dentro del WorkRect del viewport principal
if (x < vp->WorkPos.x) x = vp->WorkPos.x;
if (y < vp->WorkPos.y) y = vp->WorkPos.y;
if (x + popup_w > vp->WorkPos.x + vp->WorkSize.x)
x = vp->WorkPos.x + vp->WorkSize.x - popup_w;
if (y + popup_h > vp->WorkPos.y + vp->WorkSize.y)
y = vp->WorkPos.y + vp->WorkSize.y - popup_h;
s_popup_anchor = ImVec2(x, y);
ImGui::SetNextWindowViewport(vp->ID); // pinear al viewport principal
ImGui::OpenPopup(popup_id);
std::lock_guard<std::mutex> lk(g_mutex);
g_unread = 0;
}
ImGui::SetNextWindowPos(s_popup_anchor, ImGuiCond_Appearing);
// Popover del inbox
ImGui::PushStyleColor(ImGuiCol_PopupBg, colors::surface);
ImGui::PushStyleColor(ImGuiCol_Border, colors::border);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, radius::md);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::sm, spacing::sm));
ImGui::SetNextWindowSize(ImVec2(360.0f, 0.0f), ImGuiCond_Appearing);
if (ImGui::BeginPopup(popup_id)) {
// Header
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::TextUnformatted("Notifications");
ImGui::PopStyleColor();
ImGui::SameLine(ImGui::GetWindowContentRegionMax().x - 50.0f);
if (ImGui::SmallButton("Clear")) {
toast_history_clear();
snapshot.clear();
}
ImGui::Separator();
if (snapshot.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::TextUnformatted("(no notifications yet)");
ImGui::PopStyleColor();
} else {
const auto now = std::chrono::steady_clock::now();
ImGui::BeginChild("##inbox_list", ImVec2(0, 320.0f));
// Mostrar mas reciente arriba
for (auto it = snapshot.rbegin(); it != snapshot.rend(); ++it) {
const auto& t = *it;
ImGui::PushStyleColor(ImGuiCol_Text, color_for(t.kind));
ImGui::TextUnformatted(glyph_for(t.kind));
ImGui::PopStyleColor();
ImGui::SameLine();
ImGui::TextWrapped("%s", t.text.c_str());
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::SameLine(ImGui::GetWindowContentRegionMax().x - 30.0f);
ImGui::TextUnformatted(format_age(t.created, now).c_str());
ImGui::PopStyleColor();
ImGui::Separator();
}
ImGui::EndChild();
}
ImGui::EndPopup();
}
ImGui::PopStyleVar(3);
ImGui::PopStyleColor(2);
}
} // namespace fn_ui
+41
View File
@@ -0,0 +1,41 @@
#pragma once
// Sistema de toasts + inbox centralizado.
// - toast_push: encola un toast efimero (~3.5s con fade-out).
// - toast_render: renderiza los toasts activos; llamar una vez por frame.
// - toast_inbox_button: icono campana con badge de no-leidos + popover con
// historial completo.
//
// Uso minimo:
// fn_ui::toast_push(fn_ui::ToastKind::Success, "Reindexed 881 functions");
//
// // Una vez por frame (despues del contenido principal):
// fn_ui::toast_render();
//
// // En la toolbar, al lado de los botones de accion:
// fn_ui::toast_inbox_button("##inbox");
namespace fn_ui {
enum class ToastKind { Info, Success, Warning, Error };
// Encola un toast y lo guarda tambien en el historial del inbox.
// Si `text` es null o vacio, se sustituye por "(no message)".
void toast_push(ToastKind kind, const char* text);
// Renderiza los toasts activos (fade-out automatico). Llamar una vez por
// frame despues del contenido principal.
void toast_render();
// Boton de inbox: campana + badge con numero de no-leidos. Al hacer click
// abre un popover con el historial completo. Tambien marca todo como leido.
// `id` debe ser unico en la ventana.
void toast_inbox_button(const char* id);
// Devuelve cuantos toasts nuevos hay desde la ultima vez que se abrio el inbox.
int toast_unread_count();
// Borra el historial completo (el popover permite hacerlo tambien con un boton).
void toast_history_clear();
} // namespace fn_ui
+64
View File
@@ -0,0 +1,64 @@
---
name: toast
kind: component
lang: cpp
domain: core
version: "1.1.0"
purity: impure
signature: "void fn_ui::toast_push(ToastKind kind, const char* text); void fn_ui::toast_render(); void fn_ui::toast_inbox_button(const char* id); int fn_ui::toast_unread_count(); void fn_ui::toast_history_clear()"
description: "Notificaciones efimeras apiladas en esquina inferior + inbox con campana (badge no-leidos) y popover con historial (50 entradas)"
tags: [imgui, toast, notification, ui, tokens]
uses_functions: ["tokens_cpp_core"]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/toast.cpp"
framework: imgui
params:
- name: kind
desc: "ToastKind: Info (azul), Success (verde), Warning (amarillo), Error (rojo)"
- name: text
desc: "Mensaje. Se copia internamente, el caller no necesita mantenerlo vivo"
output: "toast_push encola un toast; toast_render renderiza todos los activos (llamar una vez por frame)"
---
# toast
Sistema global de notificaciones efimeras. Duracion ~3.5s con fade-out. Cola thread-safe (std::mutex) — se puede hacer push desde callbacks async.
## Patron
```cpp
// 1) Push desde cualquier handler (tras terminar una operacion):
fn_ui::toast_push(fn_ui::ToastKind::Success, "Reindexed 881 functions");
// 2) Render una vez por frame en el bucle principal, DESPUES del contenido:
// (asi los toasts se superponen a ventanas y modales)
render_dashboard();
fn_ui::toast_render();
```
## Inbox (v1.1)
Los toasts empujados tambien se guardan en un historial de 50 entradas. `toast_inbox_button(id)` dibuja un boton con icono de campana y un badge rojo con el numero de no-leidos; al hacer click abre un popover con el historial ordenado por fecha (mas reciente arriba) y un boton "Clear".
```cpp
// En la toolbar, al lado de Reindex/Add/Reload:
fn_ui::toast_inbox_button("##inbox");
```
El `id` debe ser unico si tienes varios botones (raro). Al abrir el popover se marcan todos como leidos (el badge desaparece hasta el siguiente `toast_push`).
## Notas
- Los toasts activos se descartan solos a los ~3.5s; el historial persiste hasta 50 entradas o hasta `toast_history_clear`.
- Pila vertical de esquina inferior derecha con gap de 8px; ancho fijo 320px.
- `NoInputs` en los toasts para que no roben foco ni bloqueen clicks sobre el contenido.
- Alpha transicional durante los ultimos 0.6s (fade-out suave).
- Si `text` es null o vacio, `toast_push` lo sustituye por "(no message)" para que el inbox nunca quede con entradas en blanco.
- La campana usa el codepoint U+1F514 (🔔). Asegurate de que la fuente ImGui tenga cargado el rango de simbolos miscellaneous symbols.
+32
View File
@@ -0,0 +1,32 @@
#include "core/toolbar.h"
#include "core/tokens.h"
#include <imgui.h>
namespace fn_ui {
void toolbar_begin() {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,
ImVec2(fn_tokens::spacing::sm, 0.0f));
ImGui::BeginGroup();
}
void toolbar_separator() {
ImGui::SameLine();
const float fh = ImGui::GetFrameHeight();
const float h = fh * 0.7f;
const ImVec2 p = ImGui::GetCursorScreenPos();
const ImU32 col = ImGui::ColorConvertFloat4ToU32(fn_tokens::colors::border);
ImGui::GetWindowDrawList()->AddLine(
ImVec2(p.x, p.y + (fh - h) * 0.5f),
ImVec2(p.x, p.y + (fh + h) * 0.5f),
col, 1.0f);
ImGui::Dummy(ImVec2(fn_tokens::spacing::xs, fh));
ImGui::SameLine();
}
void toolbar_end() {
ImGui::EndGroup();
ImGui::PopStyleVar();
}
} // namespace fn_ui
+29
View File
@@ -0,0 +1,29 @@
#pragma once
// Toolbar horizontal para agrupar acciones con spacing consistente.
// Pattern begin/separator/end. Entre begin y end el caller inserta los
// botones; entre grupos puede llamar a toolbar_separator para una linea
// vertical sutil con spacing.
//
// Uso:
// toolbar_begin();
// button("New");
// button("Open");
// toolbar_separator();
// icon_button("##settings", glyph_settings);
// toolbar_end();
namespace fn_ui {
// Abre un grupo horizontal con spacing sm entre items. Aplica
// SameLine automatico a los widgets siguientes.
// Llamar siempre a toolbar_end() aunque haya early return.
void toolbar_begin();
// Separador vertical con padding horizontal. Llamar entre grupos.
void toolbar_separator();
// Cierra el grupo. Restaura el cursor a nueva linea despues.
void toolbar_end();
} // namespace fn_ui
+52
View File
@@ -0,0 +1,52 @@
---
name: toolbar
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "void fn_ui::toolbar_begin(); void fn_ui::toolbar_separator(); void fn_ui::toolbar_end()"
description: "Grupo horizontal de acciones (buttons/icon_buttons) con spacing coherente y separadores verticales. Patron begin/separator/end."
tags: [imgui, ui, toolbar, layout, tokens]
uses_functions: ["tokens_cpp_core"]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/toolbar.cpp"
framework: imgui
params: []
output: "Inicializa/cierra un grupo horizontal con spacing sm; toolbar_separator dibuja una linea vertical sutil entre grupos"
---
# toolbar
Contenedor horizontal para agrupar acciones con spacing consistente. El caller es responsable de llamar a `ImGui::SameLine()` entre widgets consecutivos — `toolbar_separator()` ya incluye `SameLine` implicito.
## Patron
```cpp
#include "core/toolbar.h"
#include "core/button.h"
#include "core/icon_button.h"
using namespace fn_ui;
toolbar_begin();
if (button("New", ButtonVariant::Primary)) { /* ... */ }
ImGui::SameLine();
if (button("Open")) { /* ... */ }
toolbar_separator();
if (icon_button("##reload", "\xe2\x86\xbb", "Reload")) { /* ... */ }
toolbar_end();
```
## Notas
- Para alinear la toolbar a la derecha del page_header usar
`ImGui::SameLine(ImGui::GetWindowWidth() - estimated_width)` antes del begin,
o usar un Table de 2 columnas con la derecha stretch.
- El separador usa `colors::border` de tokens y ocupa 70% de `GetFrameHeight`.
+86
View File
@@ -0,0 +1,86 @@
#include "core/tree_view.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cstdio>
namespace fn_ui {
namespace {
bool g_last_clicked = false;
ImGuiTreeNodeFlags base_flags() {
return ImGuiTreeNodeFlags_SpanFullWidth
| ImGuiTreeNodeFlags_OpenOnArrow
| ImGuiTreeNodeFlags_OpenOnDoubleClick;
}
void push_node_colors(bool selected) {
using namespace fn_tokens::colors;
// Fondo y hover — el Selected usa Header color por convencion de ImGui
ImGui::PushStyleColor(ImGuiCol_Header, selected ? surface_hover : ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, surface_hover);
ImGui::PushStyleColor(ImGuiCol_HeaderActive, surface);
ImGui::PushStyleColor(ImGuiCol_Text, selected ? fn_tokens::colors::primary : fn_tokens::colors::text);
}
void pop_node_colors() {
ImGui::PopStyleColor(4);
}
} // namespace
void tree_leaf(const char* id, const char* label, bool selected) {
push_node_colors(selected);
char full[256];
std::snprintf(full, sizeof(full), "%s##%s", label, id);
ImGuiTreeNodeFlags flags = base_flags()
| ImGuiTreeNodeFlags_Leaf
| ImGuiTreeNodeFlags_NoTreePushOnOpen
| ImGuiTreeNodeFlags_Bullet;
if (selected) flags |= ImGuiTreeNodeFlags_Selected;
ImGui::TreeNodeEx(full, flags);
g_last_clicked = ImGui::IsItemClicked();
pop_node_colors();
}
bool tree_branch_begin(const char* id, const char* label, bool selected) {
push_node_colors(selected);
char full[256];
std::snprintf(full, sizeof(full), "%s##%s", label, id);
ImGuiTreeNodeFlags flags = base_flags();
if (selected) flags |= ImGuiTreeNodeFlags_Selected;
const bool open = ImGui::TreeNodeEx(full, flags);
g_last_clicked = ImGui::IsItemClicked();
pop_node_colors();
return open;
}
void tree_branch_end() {
ImGui::TreePop();
}
void tree_root(const char* id, const char* label, bool selected) {
// Igual que leaf pero sin bullet ni indent
push_node_colors(selected);
char full[256];
std::snprintf(full, sizeof(full), "%s##%s", label, id);
ImGuiTreeNodeFlags flags = base_flags()
| ImGuiTreeNodeFlags_Leaf
| ImGuiTreeNodeFlags_NoTreePushOnOpen;
if (selected) flags |= ImGuiTreeNodeFlags_Selected;
ImGui::TreeNodeEx(full, flags);
g_last_clicked = ImGui::IsItemClicked();
pop_node_colors();
}
bool tree_node_clicked() {
const bool r = g_last_clicked;
g_last_clicked = false;
return r;
}
} // namespace fn_ui
+45
View File
@@ -0,0 +1,45 @@
#pragma once
// Tree view minimo para jerarquias tipo projects -> apps/analysis/vaults.
// API low-level (begin/end por nodo) que permite al caller construir el
// arbol con logica propia (filtrado, lazy load, etc.).
//
// Uso:
// if (fn_ui::tree_node_begin("All", selected_id == "")) {
// // seleccionado via click
// if (fn_ui::tree_node_clicked()) selected_id = "";
// }
// fn_ui::tree_node_end();
//
// for (const auto& p : projects) {
// if (fn_ui::tree_branch_begin(p.id.c_str(), p.name.c_str(),
// selected_id == p.id)) {
// if (fn_ui::tree_node_clicked()) selected_id = p.id;
//
// for (const auto& a : p.apps) {
// fn_ui::tree_leaf(a.id.c_str(), a.name.c_str(),
// selected_id == a.id);
// if (fn_ui::tree_node_clicked()) selected_id = a.id;
// }
// fn_ui::tree_branch_end();
// }
// }
namespace fn_ui {
// Hoja (sin hijos). Muestra indentacion + label. Resaltado si `selected`.
void tree_leaf(const char* id, const char* label, bool selected = false);
// Rama con hijos. Devuelve true si esta desplegada (el caller dibuja los
// hijos y llama tree_branch_end). Si devuelve false, NO llamar end.
bool tree_branch_begin(const char* id, const char* label, bool selected = false);
void tree_branch_end();
// Nodo raiz "All" / "Orphans" etc. — como leaf pero sin indentacion.
void tree_root(const char* id, const char* label, bool selected = false);
// True si el ultimo nodo (leaf/branch/root) fue clickeado este frame.
// Debe consultarse inmediatamente despues de llamar al nodo.
bool tree_node_clicked();
} // namespace fn_ui
+66
View File
@@ -0,0 +1,66 @@
---
name: tree_view
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: impure
signature: "void fn_ui::tree_leaf(id, label, selected); bool fn_ui::tree_branch_begin(id, label, selected); void fn_ui::tree_branch_end(); void fn_ui::tree_root(id, label, selected); bool fn_ui::tree_node_clicked()"
description: "Tree view low-level para jerarquias (projects -> apps/analysis/vaults). Estilo basado en tokens, helper tree_node_clicked para detectar seleccion."
tags: [imgui, tree, navigation, sidebar, ui, tokens]
uses_functions: ["tokens_cpp_core"]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/tree_view.cpp"
framework: imgui
params: []
output: "Renderiza nodos de arbol; tree_node_clicked devuelve si el ultimo nodo dibujado fue clickeado este frame"
---
# tree_view
Tree view minimo construido sobre `ImGui::TreeNodeEx`, con estilo consistente de tokens: hover/selected usan `surface_hover`, el nodo seleccionado muestra texto en `primary`.
Sin logica de seleccion interna — el caller gestiona el estado y pasa `selected` por parametro. Permite filtrar, lazy-load, DnD, etc. sin que el primitive imponga politicas.
## Patron
```cpp
#include "core/tree_view.h"
using namespace fn_ui;
static std::string selected_id = "";
tree_root("all", "All projects", selected_id.empty());
if (tree_node_clicked()) selected_id = "";
for (const auto& p : projects) {
bool sel = (selected_id == p.id);
if (tree_branch_begin(p.id.c_str(), p.name.c_str(), sel)) {
if (tree_node_clicked()) selected_id = p.id;
// Subcategorias
tree_branch_begin((p.id + "/apps").c_str(), "Apps");
for (const auto& a : p.apps) {
tree_leaf(a.id.c_str(), a.name.c_str(), selected_id == a.id);
if (tree_node_clicked()) selected_id = a.id;
}
tree_branch_end();
} else {
// Si no esta desplegado, todavia podemos haber clickeado
if (tree_node_clicked()) selected_id = p.id;
}
}
```
## Notas
- `tree_branch_begin` devuelve true solo si esta desplegado. Si devuelve false no llamar `tree_branch_end`.
- `tree_node_clicked()` hay que consultarlo **inmediatamente** despues del nodo — se pisa con el siguiente.
- El id ImGui lo forma internamente como `"label##id"` para que label pueda repetirse entre proyectos.