feat(cpp/core): icon_font + icons_tabler

icon_font_cpp_core (impure): carga Karla-Regular como texto vectorial y
mergea Tabler Icons al mismo tamano en el atlas de ImGui. Tras el call,
los TI_* renderizan inline con el texto.

icons_tabler.h: header con macros TI_<NAME> que apuntan a los codepoints
del set Tabler (UTF-8 escapado). Generado a partir del CSS del vendor con
cpp/vendor/tabler-icons/gen_header.py — re-ejecutable si se actualiza el
set sin tocar a mano los ~5500 codepoints.

Justifica la regla cpp_icons.md: todas las apps C++ usan TI_* en lugar de
emoji o hex inline.
This commit is contained in:
2026-04-25 21:25:25 +02:00
parent bae4f45268
commit 5d5b1d3fea
4 changed files with 5361 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
#include "icon_font.h"
#include "app_settings.h"
#include "imgui.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#ifndef FN_CPP_ROOT
#define FN_CPP_ROOT ""
#endif
namespace fn_ui {
namespace {
bool g_text_loaded = false;
bool g_tabler_loaded = false;
bool file_exists(const char* path) {
if (!path || !*path) return false;
if (FILE* f = std::fopen(path, "rb")) { std::fclose(f); return true; }
return false;
}
// Busca un asset (TTF) en las rutas estandar del registry. Devuelve la primera
// ruta valida o vacio.
//
// Orden: ./<filename> → ./assets/<filename> → $FN_ASSETS_DIR/<filename>
// → ${FN_CPP_ROOT}/<repo_subpath>
std::string find_asset(const char* filename, const char* repo_subpath) {
std::string p;
p = std::string("./") + filename; if (file_exists(p.c_str())) return p;
p = std::string("./assets/") + filename; if (file_exists(p.c_str())) return p;
if (const char* env = std::getenv("FN_ASSETS_DIR")) {
p = std::string(env) + "/" + filename;
if (file_exists(p.c_str())) return p;
}
if (std::strlen(FN_CPP_ROOT) > 0 && repo_subpath) {
p = std::string(FN_CPP_ROOT) + "/" + repo_subpath;
if (file_exists(p.c_str())) return p;
}
return std::string();
}
const char* repo_subpath_for(FontId id) {
switch (id) {
case FontId::Karla: return "vendor/imgui/misc/fonts/Karla-Regular.ttf";
case FontId::Roboto: return "vendor/imgui/misc/fonts/Roboto-Medium.ttf";
case FontId::DroidSans: return "vendor/imgui/misc/fonts/DroidSans.ttf";
case FontId::Cousine: return "vendor/imgui/misc/fonts/Cousine-Regular.ttf";
case FontId::ProggyClean: return ""; // bitmap default
}
return "";
}
} // namespace
void load_fonts_from_settings() {
ImGuiIO& io = ImGui::GetIO();
io.Fonts->Clear();
const AppSettings& s = settings();
const float size_px = s.font_size_px;
// 1. Texto.
g_text_loaded = false;
if (s.font_id == FontId::ProggyClean) {
// Bitmap default: ignora size_px (ProggyClean es 13 px nativo).
io.Fonts->AddFontDefault();
} else {
const char* fname = font_filename(s.font_id);
std::string ttf = find_asset(fname, repo_subpath_for(s.font_id));
if (!ttf.empty()) {
ImFontConfig cfg;
cfg.OversampleH = 2;
cfg.OversampleV = 1;
cfg.PixelSnapH = false;
if (io.Fonts->AddFontFromFileTTF(ttf.c_str(), size_px, &cfg)) {
g_text_loaded = true;
} else {
std::fprintf(stderr, "[fn_ui] AddFontFromFileTTF fallo (%s)\n", ttf.c_str());
io.Fonts->AddFontDefault();
}
} else {
std::fprintf(stderr,
"[fn_ui] %s no encontrado — fallback a ProggyClean. "
"Buscado en: ./%s, ./assets/, $FN_ASSETS_DIR, %s/vendor/imgui/misc/fonts/\n",
fname, fname, FN_CPP_ROOT[0] ? FN_CPP_ROOT : "(FN_CPP_ROOT no definido)");
io.Fonts->AddFontDefault();
}
}
// 2. Iconos Tabler — mergea sobre la fuente activa.
std::string ico = find_asset("tabler-icons.ttf", "vendor/tabler-icons/tabler-icons.ttf");
if (ico.empty()) {
std::fprintf(stderr,
"[fn_ui] tabler-icons.ttf no encontrado — los TI_* saldran como cuadritos. "
"Buscado en: ./tabler-icons.ttf, ./assets/, $FN_ASSETS_DIR, %s/vendor/tabler-icons/\n",
FN_CPP_ROOT[0] ? FN_CPP_ROOT : "(FN_CPP_ROOT no definido)");
g_tabler_loaded = false;
return;
}
static const ImWchar tabler_ranges[] = {0xE000, 0xFCFF, 0};
// Tabler matchea el tamaño de texto, salvo en ProggyClean (13 px) donde lo
// forzamos a 13 tambien para no romper line-height.
const float tabler_px = (s.font_id == FontId::ProggyClean) ? 13.0f : size_px;
ImFontConfig icfg;
icfg.MergeMode = true;
icfg.PixelSnapH = true;
icfg.OversampleH = icfg.OversampleV = 1;
icfg.GlyphMinAdvanceX = tabler_px;
icfg.GlyphOffset.y = 1.0f;
if (io.Fonts->AddFontFromFileTTF(ico.c_str(), tabler_px, &icfg, tabler_ranges)) {
g_tabler_loaded = true;
} else {
std::fprintf(stderr, "[fn_ui] AddFontFromFileTTF tabler fallo (%s)\n", ico.c_str());
g_tabler_loaded = false;
}
}
bool text_font_loaded() { return g_text_loaded; }
bool tabler_font_loaded() { return g_tabler_loaded; }
} // namespace fn_ui
+24
View File
@@ -0,0 +1,24 @@
#pragma once
// Carga de fuentes para apps ImGui del registry: TTF de texto vectorial
// (Karla / Roboto / DroidSans / Cousine, segun core/app_settings.h) +
// fuente de iconos Tabler mergeada en el mismo ImFont.
//
// Llamar desde fn::run_app despues de ImGui::CreateContext y de
// settings_load(). Tras esta llamada, los TI_* (core/icons_tabler.h) se
// renderizan inline con el texto al tamaño activo.
namespace fn_ui {
// Carga la fuente de texto + Tabler usando los valores actuales de
// fn_ui::settings() (font_id + font_size_px). Llamada por run_app al inicio
// y de nuevo cuando hay font dirty (rebuild de atlas).
void load_fonts_from_settings();
// True si la ultima carga incluyo una TTF de texto (no fallback ProggyClean).
bool text_font_loaded();
// True si la ultima carga incluyo Tabler.
bool tabler_font_loaded();
} // namespace fn_ui
+96
View File
@@ -0,0 +1,96 @@
---
name: icon_font
kind: function
lang: cpp
domain: core
version: "1.0.0"
purity: impure
signature: "void fn_ui::load_default_fonts(float size_px = 13.0f)"
description: "Carga Karla-Regular (texto vectorial) + mergea Tabler Icons al mismo tamaño en el atlas de ImGui. Tras esta llamada los TI_* (icons_tabler.h) renderizan inline con el texto."
tags: [imgui, fonts, icons, tabler, atlas, init]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [imgui, cstdio, cstdlib, cstring, string]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/icon_font.cpp"
framework: imgui
params:
- name: size_px
desc: "Tamaño en px compartido por texto e iconos. Default 13 = ImGui default historico, render vectorial nitido en Karla y Tabler. El icon merge cuadra el line-height con el texto al usar el mismo tamaño"
output: "void — texto + iconos quedan activos en io.Fonts. Si Karla no se encuentra, fallback a ProggyClean default; si Tabler no se encuentra, los TI_* salen como cuadritos. Estado consultable via text_font_loaded() y tabler_font_loaded()"
---
# icon_font
Carga la fuente de texto + iconos Tabler en el atlas de ImGui de forma que `TI_*` (de `core/icons_tabler.h`) y texto se mezclen en el mismo glyph stream:
```cpp
icon_button("##rl", TI_REFRESH, "Reload");
button(TI_DEVICE_FLOPPY " Save", V::Primary); // icono + texto en la misma label
```
## Cuando llamarla
Una sola vez por contexto ImGui, despues de `ImGui::CreateContext()` y antes del primer `ImGui::NewFrame()`. La llamada ya esta hecha automaticamente por `fn::run_app` (`framework/app_base.cpp`). En apps que NO usan `fn::run_app`, llamar manualmente:
```cpp
ImGui::CreateContext();
fn_ui::load_default_fonts();
// ... resto del setup
```
## Fuentes cargadas
| Rol | TTF | Origen | Render |
|---|---|---|---|
| Texto | `Karla-Regular.ttf` (17 KB) | `cpp/vendor/imgui/misc/fonts/` (vendoreado por ImGui) | vectorial, humanist sans-serif, OversampleH=2 |
| Iconos | `tabler-icons.ttf` (2.7 MB) | `cpp/vendor/tabler-icons/` (Tabler v3.41.1, MIT) | vectorial, mergeado en el mismo `ImFont` con `MergeMode = true`, range U+E000..U+FCFF |
Los dos al mismo `size_px` para que el line-height sea uniforme. ImGui no aplica hinting nativo, pero Karla a 13 px con `OversampleH=2` queda nitida.
## Resolucion del path de las TTF
Para cada TTF, busca en este orden (primer match gana):
1. `./<filename>` (cwd / junto al exe en deploys)
2. `./assets/<filename>`
3. `$FN_ASSETS_DIR/<filename>` (override manual)
4. `${FN_CPP_ROOT}/<repo_subpath>` — compile-time define inyectado por CMake:
- Karla → `vendor/imgui/misc/fonts/Karla-Regular.ttf`
- Tabler → `vendor/tabler-icons/tabler-icons.ttf`
Si Karla no se encuentra, fallback automatico a `io.Fonts->AddFontDefault()` (ProggyClean bitmap) — la app arranca igual, solo cambia la tipografia. Si Tabler no se encuentra, los `TI_*` salen como cuadritos vacios pero el texto sigue funcionando.
## CMake
Un app que use `add_imgui_app` ya hereda el define `FN_CPP_ROOT` y la copia post-build de la TTF. Si compilas a mano:
```cmake
target_compile_definitions(my_app PRIVATE FN_CPP_ROOT="${CMAKE_SOURCE_DIR}")
```
## Range cubierto
`U+E000..U+FCFF` — Private Use Area, donde Tabler v3.41 ubica todos sus glyphs (rango real efectivo aprox U+EA5E..U+FC93, pero cubrimos toda la PUA por margen).
## Tamaño del atlas
Cargar 5093 iconos a 13 px crea un atlas de aprox 2048x2048 px en memoria de GPU. Si necesitas un atlas mas pequeño, edita el array `tabler_ranges[]` en `.cpp` para cubrir solo los iconos que uses (ej: `{0xEA5E, 0xEC00, 0}`).
## Ejemplo
```cpp
#include "core/icons_tabler.h"
#include "core/button.h"
#include "core/icon_button.h"
if (button(TI_PLUS " New", V::Primary)) create();
if (button(TI_DEVICE_FLOPPY " Save", V::Secondary)) save();
if (icon_button("##del", TI_TRASH, "Delete")) confirm();
if (icon_button("##cfg", TI_SETTINGS, "Settings")) open_settings();
```
File diff suppressed because it is too large Load Diff