Files
fn_registry/cpp/DESIGN_SYSTEM.md
egutierrez f61d2834e8 docs(cpp): añadir DESIGN_SYSTEM.md
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.
2026-04-25 21:26:09 +02:00

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.md en 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 fija app_base
  • No ImVec4(...) hardcoded en widgets — usar fn_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 gestiona AppConfig
  • 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 de apply_dark_theme; el tema no pisa io.Fonts)
  • Ni ImPlot colormaps/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.hfn::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.cpp llama a fn::run_app(...) con ThemeMode::FnDark (default) — no ImGui::StyleColorsDark
  • Cero ImVec4(0.x, 0.y, ..., 1.0f) — solo fn_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.md con frontmatter (name, description, tags, lang: cpp)
  • CMakeLists usa add_imgui_app(...) — no add_executable directo
  • 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::md ahora es 8 (antes 5) para casar con defaultRadius: 'md'
  • app_base::AppConfig añade campo theme (default FnDark) y aplica el tema en run_app
  • fn_framework incluye tokens.cpp — cualquier app que enlace el framework obtiene la identidad por default
  • fn_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_dashboard y shaders_lab ahora 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 → usar TI_SETTINGS.
  • Emojis Unicode (✓, ⚠, , ⚙) → muchos no estan en el atlas y salen como ? o cuadritos.
  • "?" / "!" ASCII como sustituto → usar TI_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.

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-trashTI_TRASH, tabler-device-floppyTI_DEVICE_FLOPPY).

Path resolution en runtime

icon_font.cpp busca Karla-Regular.ttf y tabler-icons.ttf con el mismo orden (primer match gana):

  1. ./<filename> (cwd / junto al exe — add_imgui_app copia ambas post-build)
  2. ./assets/<filename>
  3. $FN_ASSETS_DIR/<filename>
  4. ${FN_CPP_ROOT}/<repo_subpath> (compile-time define):
    • Karla → vendor/imgui/misc/fonts/Karla-Regular.ttf
    • Tabler → vendor/tabler-icons/tabler-icons.ttf

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 — solo TI_* de core/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.