Documenta el sistema de design tokens del registry C++: - Identidad visual unica (Mantine v9 dark + indigo) compartida entre apps - Como se aplican los tokens via fn::run_app - Convenciones de uso (cuando usar tokens.colors.surface vs accent, etc.) Es la fuente de verdad cuando se crean nuevas primitivas o apps fn_ui.
19 KiB
fn_registry C++ — Design System
Fuente de verdad de la identidad visual para todas las apps C++ del registry. Espejo directo de
frontend/DESIGN_SYSTEM.mden mundo ImGui / OpenGL.
1. Principio
Todas las apps C++ del registry comparten la misma identidad visual que las apps web: Mantine v9 dark + indigo primary + Geist typography + radius.md = 8 por defecto. No existen apps C++ con tema propio — si una app quiere diferenciarse, lo hace eligiendo colores semanticos (success, warning, primary...), nunca redefiniendo la paleta base.
El equivalente C++ de FnMantineProvider + createTheme(...) es una sola llamada:
fn::run_app(config, render); // AppConfig::theme = ThemeMode::FnDark por defecto
Dentro, app_base invoca fn_tokens::apply_dark_theme() tras crear el contexto ImGui. Equivale exactamente a:
<FnMantineProvider theme={createTheme({
primaryColor: 'indigo',
primaryShade: { light: 6, dark: 4 },
defaultRadius: 'md',
})} defaultColorScheme="dark">
2. Stack
| Capa | Libreria | Notas |
|---|---|---|
| UI base | Dear ImGui (vendored) | immediate mode, docking branch |
| Charts | ImPlot (vendored) | line/bar/pie/histogram/heatmap/sparkline |
| Node editor | imgui-node-editor (vendored) |
solo para shader_canvas / DAGs |
| Gráficos gfx | OpenGL 3.3 core via GLFW | gl_shader, gl_framebuffer, fullscreen_quad |
| Profiling | Tracy (opt-in con TRACY_ENABLE=ON) |
tracy_zone_cpp_core |
| Tokens | cpp/functions/core/tokens.{h,cpp} |
equivalente a @fn_library |
| Componentes | cpp/functions/core/*, viz/*, gfx/* |
wrappers que leen fn_tokens |
| Framework | cpp/framework/app_base.{h,cpp} |
bootstrap unico de GLFW + ImGui + tema |
| Build | CMake 3.16+ | un add_subdirectory por app |
Deny-list (no usar en apps C++)
- No
ImGui::StyleColorsDark/Light/Classic()en el main de una app — el tema lo fijaapp_base - No
ImVec4(...)hardcoded en widgets — usarfn_tokens::colors::* - No pixeles magicos para spacing/radius — usar
fn_tokens::spacing::*/radius::* - No ICU / gettext — textos directos, UTF-8 en el fuente
- No libs de UI ajenas (Qt, Nuklear, Gtk, etc.)
- No CSS, QSS ni recursos externos de estilo
3. AppConfig y modos de tema
#include "app_base.h"
int main() {
fn::AppConfig cfg;
cfg.title = "my_app";
cfg.width = 1400;
cfg.height = 900;
cfg.viewports = true; // multi-viewport OK, el tema se mantiene
// cfg.theme = fn::ThemeMode::FnDark; // ya es el default
return fn::run_app(cfg, render);
}
ThemeMode |
Uso |
|---|---|
FnDark (default) |
Identidad del registry — Mantine v9 dark + indigo |
ImGuiDark |
Dark de stock (gris, rounding 0). Solo si estas depurando estilos |
ImGuiLight |
Light de stock. Para capturas o documentacion |
None |
No toca ImGuiStyle. La app se encarga (evitar salvo caso excepcional) |
Regla: nunca cambies ThemeMode salvo que tengas justificacion; si tu app necesita variaciones, expon-las como props semanticos (kind: "danger", variant: "subtle") en tus widgets, no como tema alternativo.
4. Tokens — fn_tokens
Todos en cpp/functions/core/tokens.h. Namespaces:
4.1 Colors — Mantine v9 dark + indigo
| Token | Mantine | Hex | Uso |
|---|---|---|---|
colors::primary |
indigo.6 | #4C6EF5 |
Botones, tabs activos, selecciones |
colors::primary_hover |
indigo.5 | #5C7CFA |
Hover de primary |
colors::primary_light |
indigo.4 | #748FFC |
Nav highlight, checkmarks, separator activo |
colors::primary_active |
indigo.7 | #4263EB |
Pulsado |
colors::success |
green.6 | #40C057 |
Badges / toasts de exito |
colors::warning |
yellow.6 | #FAB005 |
Avisos |
colors::error |
red.6 | #FA5252 |
Errores, destructive |
colors::info |
blue.6 | #228BE6 |
Info, enlaces |
colors::bg |
dark.7 | #1A1B1E |
Window bg (= Mantine body) |
colors::surface |
dark.6 | #25262B |
Paper, Card, FrameBg |
colors::surface_hover |
dark.5 | #2C2E33 |
Hover de surface |
colors::surface_active |
dark.4 | #373A40 |
Pulsado de surface |
colors::border |
dark.4 | #373A40 |
Borders estandar |
colors::border_strong |
dark.3 | #5C5F66 |
Scrollbar grab active |
colors::text |
dark.0 | #C1C2C5 |
Texto primario |
colors::text_muted |
dark.2 | #909296 |
Subtitulos, axis labels |
colors::text_dim |
dark.3 | #5C5F66 |
Disabled |
4.2 Radius — defaultRadius: 'md'
| Token | Valor | Equivalente Mantine |
|---|---|---|
radius::none |
0 | — |
radius::xs |
2 | --mantine-radius-xs |
radius::sm |
4 | --mantine-radius-sm |
radius::md |
8 | --mantine-radius-md (default) |
radius::lg |
12 | --mantine-radius-lg (adaptado, Mantine usa 16) |
radius::xl |
16 | --mantine-radius-xl (adaptado, Mantine usa 32) |
ImGui aplica md en WindowRounding, ChildRounding, PopupRounding, FrameRounding, ScrollbarRounding. sm en GrabRounding, TabRounding.
4.3 Spacing — densificado para ImGui
Mantine usa 10/12/16/20/32 px para CSS; ImGui vive en densidad mayor (TUI-ish), asi que el mapeo se densifica:
| Token | ImGui | Mantine CSS |
|---|---|---|
spacing::xs |
4 | 10 |
spacing::sm |
8 | 12 |
spacing::md |
12 | 16 |
spacing::lg |
16 | 20 |
spacing::xl |
24 | 32 |
Se usan en WindowPadding, FramePadding, ItemSpacing, CellPadding, IndentSpacing.
4.4 Font size
font_size::xs|sm|md|lg|xl|xxl = 10|12|14|18|24|32. El default (14) coincide con --mantine-font-size-sm (tiempos mas compactos que el body web de 16).
5. Qué aplica apply_dark_theme()
Se invoca una vez desde app_base (o manualmente si usas ThemeMode::None). Configura:
- Todos los
ImGuiCol_*: WindowBg, ChildBg, PopupBg, FrameBg (+Hovered/Active), Button (+Hovered/Active), Header (+Hovered/Active), Tab (+Hovered/Active/Unfocused), Separator (+Hovered/Active), Border, Text (+Disabled/Selected), CheckMark, SliderGrab (+Active), ResizeGrip (+Hovered/Active), Scrollbar (+Grab +GrabHovered +GrabActive), Table (Header +BorderLight +BorderStrong +RowBg +RowBgAlt), Docking (Preview +EmptyBg), PlotLines/PlotHistogram, DragDropTarget, Nav (Highlight +WindowingHighlight +WindowingDimBg), ModalWindowDimBg, TextSelectedBg, MenuBarBg, TitleBg (+Active +Collapsed) - Rounding: Window, Child, Popup, Frame, Grab, Scrollbar, Tab
- Padding/spacing: WindowPadding, FramePadding, CellPadding, ItemSpacing, ItemInnerSpacing, IndentSpacing, ScrollbarSize, GrabMinSize
- Borders: WindowBorderSize/ChildBorderSize/PopupBorderSize = 1, FrameBorderSize/TabBorderSize = 0 (Mantine no pinta border en frames)
- ImPlot si esta linkado: FrameBg, PlotBg, PlotBorder, LegendBg/Border/Text, TitleText, InlayText, AxisText/Grid/Tick/Bg, Selection, Crosshairs + paddings
Qué NO toca (capacidades ImGui intactas)
- Ni
io.ConfigFlags(docking, viewports, keyboard nav, multi-viewport): los gestionaAppConfig - Ni backends (GLFW/OpenGL init): los gestiona
app_base - Ni
ImGui::CreateContext/DestroyContext - Ni fuentes (si quieres Geist o una font custom, llama
io.Fonts->AddFontFromFileTTF(...)antes deapply_dark_theme; el tema no pisaio.Fonts) - Ni
ImPlotcolormaps/markers (solo colores de chrome)
6. Equivalencias @fn_library ↔ C++
Web (@fn_library) |
C++ (cpp/functions/...) |
Notas |
|---|---|---|
FnMantineProvider + createTheme |
fn::run_app con ThemeMode::FnDark |
Punto de entrada unico |
Button |
core/button.h — fn::core::button(label, kind, size) |
kind: Primary|Secondary|Subtle|Danger, size: Sm|Md|Lg |
FnActionIcon |
core/icon_button.h |
28x28 + tooltip |
Badge |
core/badge.h |
Default|Success|Warning|Error|Info|Outline |
Input / TextInput |
core/text_input.h |
label muted arriba |
Select |
core/select.h |
|
Dialog |
core/modal_dialog.h |
patron begin/end |
Toast / notifications.show |
core/toast.h |
stack bottom-right con fade-out |
PageHeader |
core/page_header.h |
begin/end para meter acciones |
EmptyState |
core/empty_state.h |
icono grande + titulo + desc |
Sheet / Drawer |
core/sidebar.h |
panel lateral colapsable |
Tabs |
core/tab_container.h |
|
AppShell |
core/docking_layout.h |
preset de docking space |
Card / Paper |
core/dashboard_panel.h |
surface + border + radius md |
SimpleGrid |
core/dashboard_grid.h |
N columnas uniformes |
FnTree / nav anidada |
core/tree_view.h |
|
KPICard |
viz/kpi_card.h |
valor + delta + sparkline |
Sparkline |
viz/sparkline.h |
mini chart inline |
LineChart |
viz/line_plot.h |
|
BarChart |
viz/bar_chart.h |
|
AreaChart |
viz/line_plot.h + flags ImPlot |
|
PieChart |
viz/pie_chart.h |
pie/donut |
DataTable |
viz/table_view.h |
sorting + scroll |
GraphContainer |
viz/graph_viewport.h + viz/graph_renderer.h |
sigma → ImGui GPU |
Tabler icons |
Glyphs UTF-8 en icon_button |
evitar bundles de icon atlas en apps pequeñas |
7. Plantilla main.cpp para una app nueva
#include "app_base.h"
#include "imgui.h"
#include "core/tokens.h"
#include "core/fullscreen_window.h"
#include "core/page_header.h"
#include "core/dashboard_grid.h"
#include "viz/kpi_card.h"
static void render() {
fullscreen_window_begin("##my_app");
page_header_begin("My App", "subtitle opcional");
page_header_end();
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
dashboard_grid_begin(4);
kpi_card("Total", "1,284", /* delta */ 12.0f, /* positive */ true);
kpi_card("OK", "97.3%", 0.4f, true);
kpi_card("P95", "842ms", -8.0f, true);
kpi_card("Err", "34", 5.0f, false);
dashboard_grid_end();
fullscreen_window_end();
}
int main() {
fn::AppConfig cfg;
cfg.title = "my_app";
cfg.width = 1400;
cfg.height = 900;
return fn::run_app(cfg, render);
}
CMakeLists minimo de la app:
add_imgui_app(my_app
main.cpp
${CMAKE_SOURCE_DIR}/functions/core/fullscreen_window.cpp
${CMAKE_SOURCE_DIR}/functions/core/page_header.cpp
${CMAKE_SOURCE_DIR}/functions/core/dashboard_grid.cpp
${CMAKE_SOURCE_DIR}/functions/viz/kpi_card.cpp
)
fn_framework ya incluye tokens.cpp, no hace falta añadirlo.
8. Anti-patrones (rechazar en review)
// ❌ Hardcode de paleta
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.4f, 0.9f, 1.0f));
// ✅
ImGui::PushStyleColor(ImGuiCol_Button, fn_tokens::colors::primary);
// ❌ StyleColorsDark en la app — sobreescribe la identidad
ImGui::StyleColorsDark();
// ❌ apply_dark_theme en cada frame (no es caro pero es ruido)
if (!theme_applied) { fn_tokens::apply_dark_theme(); ... }
// ✅ run_app lo hace una vez al inicio
// ❌ Spacing magico
ImGui::Dummy(ImVec2(0, 13));
// ✅
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
// ❌ Mezclar Qt, Nuklear u otra lib de UI en apps/
// ✅ ImGui + ImPlot + los wrappers de cpp/functions/
// ❌ Meter un sub-tema para "destacar" una pantalla
cfg.theme = fn::ThemeMode::ImGuiLight; // romperia consistencia
// ✅ usar tokens semanticos (success/warning/...)
9. Checklist para una app C++ nueva
main.cppllama afn::run_app(...)conThemeMode::FnDark(default) — noImGui::StyleColorsDark- Cero
ImVec4(0.x, 0.y, ..., 1.0f)— solofn_tokens::colors::* - Cero floats magicos en padding/spacing/rounding — solo
fn_tokens::spacing::*/radius::* - Componentes comunes desde
cpp/functions/core/,viz/,gfx/ - Si creas un widget reutilizable, extraelo a
cpp/functions/<domain>/{name}.{h,cpp,md}y./fn index app.mdcon frontmatter (name,description,tags,lang: cpp)- CMakeLists usa
add_imgui_app(...)— noadd_executabledirecto - Multi-viewport (
cfg.viewports = true) si la app espera ser undock-able; el tema se adapta solo - Tracy opt-in solo si interesa profiling (
-DTRACY_ENABLE=ON)
10. Cambios respecto al estado previo (2026-04)
- Tokens alineados a valores exactos de Mantine v9 dark + indigo (antes aproximaban a ojo)
radius::mdahora es 8 (antes 5) para casar condefaultRadius: 'md'app_base::AppConfigañade campotheme(defaultFnDark) y aplica el tema enrun_appfn_frameworkincluyetokens.cpp— cualquier app que enlace el framework obtiene la identidad por defaultfn_tokens::apply_dark_theme()ahora estiliza tambien ImPlot si esta enlazado (colores de plot bg, axis, legend, crosshairs) y añade entradas que faltaban (docking preview, nav highlight, modal dim, scrollbars)registry_dashboardyshaders_labahora comparten look 100% sin cambios en sus propios mains
11. Iconos — Tabler (auto-cargado)
Espejo del frontend web (@tabler/icons-react). Set unico para todas las apps C++ del registry. Sin glyph atlas custom por app, sin emojis Unicode arbitrarios.
Regla
Cualquier glyph en boton, menu, toolbar, tree o status line DEBE salir de los macros TI_* de cpp/functions/core/icons_tabler.h. No se permite:
"\xe2\x9a\x99"o cualquier UTF-8 hex inline → usarTI_SETTINGS.- Emojis Unicode (✓, ⚠, ❌, ⚙) → muchos no estan en el atlas y salen como
?o cuadritos. "?"/"!"ASCII como sustituto → usarTI_HELP,TI_ALERT_CIRCLE.
Como funciona
fn::run_app (en framework/app_base.cpp) llama a fn_ui::load_default_fonts() justo despues de crear el contexto ImGui. Carga Karla-Regular (texto vectorial, 13 px, vendoreado por ImGui en vendor/imgui/misc/fonts/) + mergea tabler-icons.ttf (5093 glyphs, U+E000..U+FCFF) al mismo tamaño en el mismo ImFont. Texto e iconos comparten line-height. A partir de ahi:
#include "core/icons_tabler.h"
button(TI_PLUS " New", V::Primary);
button(TI_DEVICE_FLOPPY " Save", V::Secondary);
icon_button("##del", TI_TRASH, "Delete");
icon_button("##cfg", TI_SETTINGS, "Settings");
Texto y glyph se mezclan en la misma string porque comparten atlas.
Catalogo
cpp/functions/core/icons_tabler.h (generado por cpp/vendor/tabler-icons/gen_header.py desde el CSS oficial v3.41.1). Buscar un icono:
grep -i "TI_TRASH\|TI_DELETE" cpp/functions/core/icons_tabler.h
# o explorar en https://tabler.io/icons (mismo set)
Nombres: TI_<NAME_UPPER_SNAKE> derivado del nombre kebab-case del icono Tabler (tabler-trash → TI_TRASH, tabler-device-floppy → TI_DEVICE_FLOPPY).
Path resolution en runtime
icon_font.cpp busca Karla-Regular.ttf y tabler-icons.ttf con el mismo orden (primer match gana):
./<filename>(cwd / junto al exe —add_imgui_appcopia ambas post-build)./assets/<filename>$FN_ASSETS_DIR/<filename>${FN_CPP_ROOT}/<repo_subpath>(compile-time define):- Karla →
vendor/imgui/misc/fonts/Karla-Regular.ttf - Tabler →
vendor/tabler-icons/tabler-icons.ttf
- Karla →
Fallbacks: si falta Karla, ImGui usa ProggyClean (bitmap, default historico). Si falta Tabler, los TI_* salen como cuadritos. Indicadores programaticos: fn_ui::text_font_loaded(), fn_ui::tabler_font_loaded().
Upgrade de version
cd cpp/vendor/tabler-icons
curl -sL -o tabler-icons.ttf "https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@<NEW_VERSION>/dist/fonts/tabler-icons.ttf"
curl -sL -o tabler-icons.css "https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@<NEW_VERSION>/dist/tabler-icons.css"
python3 gen_header.py # regenera ../../functions/core/icons_tabler.h
Bump tambien TI_FONT_VERSION queda actualizado automaticamente.
Anti-patrones (rechazar en review)
// ❌ Hex UTF-8 inline — pre-Tabler, no renderiza en el atlas actual
icon_button("##rl", "\xe2\x86\xbb", "Reload");
// ✅
icon_button("##rl", TI_REFRESH, "Reload");
// ❌ Emoji
button("✓ OK", V::Primary);
// ✅
button(TI_CHECK " OK", V::Primary);
// ❌ Cargar otra fuente de iconos en el app (FontAwesome, Material...)
io.Fonts->AddFontFromFileTTF("fa-solid.ttf", ...);
// ✅ usar TI_* — ya esta cargado por fn::run_app
// ❌ Bypass del path resolver hardcodeando una ruta absoluta
io.Fonts->AddFontFromFileTTF("/home/lucas/.../tabler.ttf", ...);
// ✅ confiar en load_default_fonts() / FN_ASSETS_DIR
Checklist (añadir a la del punto 9)
- Cero
"\x..\x.."en codigo de UI — soloTI_*decore/icons_tabler.h - Cero emojis Unicode en strings de boton / menu / toolbar
- Si el icono que necesito no existe en Tabler, replantear el flujo (en general hay equivalente)
- No cargar fuentes adicionales sin justificacion (el atlas merge es delicado)
12. Settings — ventana flotante + persistencia
Toda app C++ del registry hereda de fn::run_app un menu Settings... en la MainMenuBar (junto a View / Layouts) que abre una ventana flotante con:
- Display → toggle
Show FPS overlay(overlay top-right con fps + ms) - Typography → combo de fuente (Karla / Roboto / DroidSans / Cousine / ProggyClean) + combo de tamaño (12/13/14/15/16/18/20 px) + slider libre 10..32 px
- Secciones extra registradas por la app (si las hay)
Persistencia — app_settings.ini
Junto al ejecutable. Auto-save al cambiar (sin botones). Cada app tiene su propio .ini porque cada exe corre desde su carpeta:
# fn_registry app_settings.ini — autogenerado, editable
show_fps = 1
font_id = 0 # 0=Karla 1=Roboto 2=DroidSans 3=Cousine 4=ProggyClean
font_size_px = 15.0
Extender desde una app
#include "core/app_settings.h"
int main() {
fn_ui::settings_window_add_section("shader_compiler", "Shader compiler", []{
ImGui::Checkbox("Auto-compile on save", &g_auto_compile);
ImGui::SliderInt("Debounce (ms)", &g_debounce_ms, 50, 2000);
});
return fn::run_app({...}, render);
}
La seccion aparece debajo de las core, dentro de un CollapsingHeader abierto por defecto. La app es responsable de persistir su estado (su SQLite, env, archivo aparte) — el .ini core solo guarda show_fps/font_id/font_size_px.
Cambio de fuente en runtime
run_app consume settings_consume_font_dirty() al inicio de cada frame: si hay cambio, ejecuta io.Fonts->Clear() + load_fonts_from_settings(). ImGui 1.92+ refresca la GPU texture automaticamente via ImGui_ImplOpenGL3_UpdateTexture al siguiente NewFrame. Coste: un frame de hiccup, sin reinicio.
Defaults
font_id = Karla— humanist sans-serif vectorial, vendoreado por ImGui (cero descargas), nitida a 13..18 px.font_size_px = 15— un punto por encima de ImGui default (13) para legibilidad sin sacrificar densidad. Si una app necesita mas detalle, el usuario lo bumpea a 18/20.show_fps = false— overlay opcional, no contamina capturas por defecto.
Anti-patron
// ❌ Fps overlay hardcoded en el render — el toggle de Settings no lo apaga
fps_overlay();
// ✅ El overlay lo gestiona run_app via settings().show_fps. La app no llama nada.
// ❌ AddFontFromFileTTF directo en main — pisa load_fonts_from_settings
io.Fonts->AddFontFromFileTTF("MyFont.ttf", 14);
// ✅ Si necesitas una fuente fija no-configurable, hazlo en una nueva FontId
// y un nuevo archivo .ttf vendoreado, no en codigo de app.