docs(flows): DoD obligatorio con user-facing surface + abrir issues 0100-0103 (taxonomia, frontmatter migration, dev_console, work dashboard)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+67
-38
@@ -248,13 +248,67 @@ function(add_imgui_app target)
|
||||
set(_rc_file ${CMAKE_CURRENT_BINARY_DIR}/${target}_appicon.rc)
|
||||
# Forward slashes para que windres no se confunda con escapes.
|
||||
file(TO_CMAKE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/appicon.ico _ico_path)
|
||||
file(WRITE ${_rc_file} "IDI_ICON1 ICON \"${_ico_path}\"\n")
|
||||
# Numeric ID 101 = FN_APP_ICON_ID (ver cpp/framework/app_base.cpp).
|
||||
# Usamos ID numerico (no string "IDI_ICON1") para que LoadImageW
|
||||
# pueda recuperarlo en runtime y attacharlo al HWND (WM_SETICON).
|
||||
file(WRITE ${_rc_file} "101 ICON \"${_ico_path}\"\n")
|
||||
list(APPEND _extra_sources ${_rc_file})
|
||||
endif()
|
||||
|
||||
# Modules manifest (issue 0097): siempre generamos <target>_modules_generated.cpp.
|
||||
# Si la app tiene app.md con uses_modules, el .cpp resultante define
|
||||
# fn::app_modules_array[] con sus modulos. Si no, genera un stub vacio
|
||||
# (apps sin app.md no rompen el linkage de framework's app_about).
|
||||
set(_modules_gen ${CMAKE_CURRENT_BINARY_DIR}/${target}_modules_generated.cpp)
|
||||
set(_codegen_script ${FN_CPP_ROOT_DIR}/../python/functions/infra/codegen_app_modules.py)
|
||||
set(_modules_root ${FN_CPP_ROOT_DIR}/../modules)
|
||||
set(_app_md ${CMAKE_CURRENT_SOURCE_DIR}/app.md)
|
||||
if(NOT EXISTS ${_app_md})
|
||||
# No app.md: emit empty stub directamente (sin invocar Python).
|
||||
file(WRITE ${_modules_gen}
|
||||
"// Auto-generated stub (no app.md).
|
||||
#include \"app_modules.h\"
|
||||
namespace fn {
|
||||
const ModuleInfo app_modules_array[1] = { { nullptr, nullptr, nullptr } };
|
||||
const unsigned long app_modules_count = 0;
|
||||
}
|
||||
")
|
||||
else()
|
||||
find_package(Python3 QUIET COMPONENTS Interpreter)
|
||||
if(Python3_FOUND AND EXISTS ${_codegen_script})
|
||||
execute_process(
|
||||
COMMAND ${Python3_EXECUTABLE} ${_codegen_script}
|
||||
--app-md ${_app_md}
|
||||
--modules-root ${_modules_root}
|
||||
--app-name ${target}
|
||||
--out ${_modules_gen}
|
||||
RESULT_VARIABLE _codegen_rc
|
||||
OUTPUT_VARIABLE _codegen_out
|
||||
ERROR_VARIABLE _codegen_err
|
||||
)
|
||||
if(NOT _codegen_rc EQUAL 0 AND NOT _codegen_rc EQUAL 2)
|
||||
message(WARNING "codegen_app_modules failed for ${target}: ${_codegen_err}")
|
||||
endif()
|
||||
endif()
|
||||
# Si python falla o el script no esta, emit stub vacio.
|
||||
if(NOT EXISTS ${_modules_gen})
|
||||
file(WRITE ${_modules_gen}
|
||||
"// Auto-generated stub (codegen unavailable).
|
||||
#include \"app_modules.h\"
|
||||
namespace fn {
|
||||
const ModuleInfo app_modules_array[1] = { { nullptr, nullptr, nullptr } };
|
||||
const unsigned long app_modules_count = 0;
|
||||
}
|
||||
")
|
||||
endif()
|
||||
endif()
|
||||
list(APPEND _extra_sources ${_modules_gen})
|
||||
|
||||
add_executable(${target} ${ARGN} ${_extra_sources})
|
||||
target_link_libraries(${target} PRIVATE fn_framework)
|
||||
target_include_directories(${target} PRIVATE
|
||||
${FN_CPP_ROOT_DIR}/functions
|
||||
${FN_CPP_ROOT_DIR}/framework
|
||||
)
|
||||
# Convencion de layout (cpp_apps.md §7):
|
||||
# <exe_dir>/<app>.exe + <app>.dll (binario + DLLs Windows convention)
|
||||
@@ -289,44 +343,13 @@ endfunction()
|
||||
# Functions are compiled as part of apps that use them via add_imgui_app.
|
||||
# Each function is a .h/.cpp pair included by the app's CMakeLists.txt.
|
||||
|
||||
# --- fn_table_viz: static lib bundling all Wave 1+2 tables-stack functions ---
|
||||
# Issue 0081-I. Apps consumidores: target_link_libraries(<app> PRIVATE fn_table_viz).
|
||||
# data_table.cpp references playground-local headers (llm_anthropic.h, tql_to_sql.h,
|
||||
# tql.h, data_table_logic.h). These are NOT available in the registry build — they
|
||||
# live in the playground. fn_table_viz excludes data_table.cpp intentionally until
|
||||
# those playground dependencies are promoted to the registry (Wave 4 deuda).
|
||||
# The remaining 9 .cpp files compile cleanly with only registry headers.
|
||||
# --- fn_module_data_table (issue 0097 modules) ---
|
||||
# Static lib defined in modules/data_table/CMakeLists.txt. Replaces former
|
||||
# fn_module_data_table target. Apps opt-in via:
|
||||
# target_link_libraries(<app> PRIVATE fn_module_data_table)
|
||||
# Lua is a hard dep — only build the module when the vendored lua tree exists.
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/vendor/lua/CMakeLists.txt)
|
||||
add_library(fn_table_viz STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/functions/core/compute_stage.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/functions/core/compute_pipeline.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/functions/core/tql_emit.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/functions/core/tql_helpers.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/functions/core/tql_apply.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/functions/core/tql_to_sql.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/functions/core/lua_engine.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/functions/core/join_tables.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/functions/core/auto_detect_type.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/functions/core/compute_column_stats.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/functions/core/llm_anthropic.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/functions/viz/viz_render.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/functions/viz/data_table.cpp
|
||||
)
|
||||
target_include_directories(fn_table_viz PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/functions
|
||||
)
|
||||
target_include_directories(fn_table_viz PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/framework
|
||||
)
|
||||
target_compile_definitions(fn_table_viz PUBLIC FN_LLM_ANTHROPIC=1)
|
||||
target_link_libraries(fn_table_viz PUBLIC
|
||||
imgui
|
||||
implot
|
||||
lua54
|
||||
)
|
||||
# fn::local_path() used by data_table.cpp (Ask AI export path + TQL save/load).
|
||||
# fn_framework provides the implementation; link it here.
|
||||
target_link_libraries(fn_table_viz PRIVATE fn_framework)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/../modules/data_table ${CMAKE_BINARY_DIR}/modules/data_table)
|
||||
endif()
|
||||
|
||||
# --- Demo app (lives in apps/, issue 0096 standardization) ---
|
||||
@@ -464,3 +487,9 @@ set(_DATA_FACTORY_DIR ${CMAKE_SOURCE_DIR}/../apps/data_factory)
|
||||
if(EXISTS ${_DATA_FACTORY_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_DATA_FACTORY_DIR} ${CMAKE_BINARY_DIR}/apps/data_factory)
|
||||
endif()
|
||||
|
||||
# --- app_hub_launcher (lives in apps/, issue 0096) ---
|
||||
set(_APP_HUB_LAUNCHER_DIR ${CMAKE_SOURCE_DIR}/../apps/app_hub_launcher)
|
||||
if(EXISTS ${_APP_HUB_LAUNCHER_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_APP_HUB_LAUNCHER_DIR} ${CMAKE_BINARY_DIR}/apps/app_hub_launcher)
|
||||
endif()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "app_base.h"
|
||||
#include "version_generated.h"
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_impl_glfw.h"
|
||||
@@ -24,6 +25,7 @@
|
||||
#include <string>
|
||||
#include <sys/stat.h>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
@@ -178,6 +180,50 @@ static void install_sizemove_subclass_hwnd(HWND hwnd) {
|
||||
g_subclassed[hwnd] = orig;
|
||||
}
|
||||
|
||||
// Resource ID generado por cpp/CMakeLists.txt en <target>_appicon.rc:
|
||||
// 101 ICON "<app_dir>/appicon.ico"
|
||||
// Si la app no tiene appicon.ico el .rc no se genera y LoadImageW devuelve
|
||||
// NULL — no error visible, los HWND quedan con el icono GLFW por defecto.
|
||||
#define FN_APP_ICON_RES_ID 101
|
||||
|
||||
// Carga el icono embebido al tamaño OS-recomendado para small (title bar) y
|
||||
// big (Alt+Tab / taskbar). LR_SHARED -> Windows gestiona el handle; no hay
|
||||
// que DestroyIcon. Cacheado por HMODULE+ID+size.
|
||||
static HICON load_app_icon(int cx, int cy) {
|
||||
HMODULE mod = GetModuleHandleW(nullptr);
|
||||
return (HICON)LoadImageW(mod, MAKEINTRESOURCEW(FN_APP_ICON_RES_ID),
|
||||
IMAGE_ICON, cx, cy, LR_SHARED | LR_DEFAULTCOLOR);
|
||||
}
|
||||
|
||||
// Adjunta el icono embebido al HWND:
|
||||
// WM_SETICON ICON_SMALL -> title bar (16x16) y Alt+Tab small variant.
|
||||
// WM_SETICON ICON_BIG -> taskbar (32x32) y Alt+Tab big variant.
|
||||
// SetClassLongPtrW propaga el icono al WNDCLASS para que nuevos HWNDs de la
|
||||
// misma clase lo hereden (no critico — el per-frame scan ya cubre cada
|
||||
// viewport secundario via su HWND propio, que puede tener WNDCLASS distinta).
|
||||
static std::unordered_set<HWND> g_icon_attached;
|
||||
static void attach_app_icon_to_hwnd(HWND hwnd) {
|
||||
if (!hwnd) return;
|
||||
if (g_icon_attached.count(hwnd)) return; // idempotent
|
||||
HICON hSmall = load_app_icon(GetSystemMetrics(SM_CXSMICON),
|
||||
GetSystemMetrics(SM_CYSMICON));
|
||||
HICON hBig = load_app_icon(GetSystemMetrics(SM_CXICON),
|
||||
GetSystemMetrics(SM_CYICON));
|
||||
if (!hSmall && !hBig) return; // no appicon.ico embebido — nada que hacer
|
||||
if (hSmall) SendMessageW(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hSmall);
|
||||
if (hBig) SendMessageW(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hBig);
|
||||
if (hSmall) SetClassLongPtrW(hwnd, GCLP_HICONSM, (LONG_PTR)hSmall);
|
||||
if (hBig) SetClassLongPtrW(hwnd, GCLP_HICON, (LONG_PTR)hBig);
|
||||
g_icon_attached.insert(hwnd);
|
||||
}
|
||||
|
||||
static void prune_dead_icon_attached() {
|
||||
for (auto it = g_icon_attached.begin(); it != g_icon_attached.end();) {
|
||||
if (!IsWindow(*it)) it = g_icon_attached.erase(it);
|
||||
else ++it;
|
||||
}
|
||||
}
|
||||
|
||||
static void install_sizemove_subclass(GLFWwindow* w) {
|
||||
if (!w) return;
|
||||
install_sizemove_subclass_hwnd(glfwGetWin32Window(w));
|
||||
@@ -337,6 +383,14 @@ void migrate_to_local_files(const char* const* names, std::size_t n) {
|
||||
}
|
||||
}
|
||||
|
||||
const char* framework_version() {
|
||||
return FN_MODULE_FRAMEWORK_VERSION;
|
||||
}
|
||||
|
||||
const char* framework_description() {
|
||||
return FN_MODULE_FRAMEWORK_DESCRIPTION;
|
||||
}
|
||||
|
||||
int run_app(AppConfig config, std::function<void()> render_fn) {
|
||||
// Logger primero para capturar fallos del propio init (GLFW, ventana, GL).
|
||||
if (config.log.file_path != nullptr) {
|
||||
@@ -401,6 +455,11 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
|
||||
// thread; we observe them and skip render+swap so the compositor moves
|
||||
// the existing buffer (same contract as native title-bar drag).
|
||||
install_sizemove_subclass(window);
|
||||
|
||||
// Adjuntar appicon embebido al HWND principal para que aparezca en la
|
||||
// barra de tareas, Alt+Tab y title bar (GLFW no propaga el icono de
|
||||
// recursos del .exe a su WNDCLASS por defecto).
|
||||
attach_app_icon_to_hwnd(glfwGetWin32Window(window));
|
||||
#endif
|
||||
|
||||
// Carga punteros a funciones GL >= 2.0 si la app lo pide. En Linux es
|
||||
@@ -565,11 +624,18 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
|
||||
// their very first frame onwards.
|
||||
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
|
||||
prune_dead_subclassed();
|
||||
prune_dead_icon_attached();
|
||||
ImGuiPlatformIO& pio_sub = ImGui::GetPlatformIO();
|
||||
for (int i = 0; i < pio_sub.Viewports.Size; ++i) {
|
||||
ImGuiViewport* vp = pio_sub.Viewports[i];
|
||||
if (!vp || !vp->PlatformHandle) continue;
|
||||
install_sizemove_subclass((GLFWwindow*)vp->PlatformHandle);
|
||||
GLFWwindow* gw = (GLFWwindow*)vp->PlatformHandle;
|
||||
install_sizemove_subclass(gw);
|
||||
// Floating panels = secondary HWNDs creados por el backend
|
||||
// GLFW. WNDCLASS distinta de la main -> no heredan icono via
|
||||
// SetClassLongPtrW. WM_SETICON per-HWND es la unica forma de
|
||||
// que el taskbar/titlebar muestren el icono.
|
||||
attach_app_icon_to_hwnd(glfwGetWin32Window(gw));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -82,6 +82,11 @@ const char* asset_path(const char* name);
|
||||
// apps lo llaman al iniciar para migrar instalaciones viejas.
|
||||
void migrate_to_local_files(const char* const* names, std::size_t n);
|
||||
|
||||
// Framework metadata (auto-generated from modules/framework/module.md via
|
||||
// `fn index`). About panel reads these.
|
||||
const char* framework_version();
|
||||
const char* framework_description();
|
||||
|
||||
// Modos de tema para run_app.
|
||||
enum class ThemeMode {
|
||||
FnDark, // Identidad del registry (Mantine v9 dark + indigo). DEFAULT.
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Module manifest visible to fn_framework's About panel.
|
||||
//
|
||||
// Each app gets an auto-generated <app>_modules_generated.cpp (codegen via
|
||||
// python/functions/infra/codegen_app_modules.py, invoked by add_imgui_app at
|
||||
// CMake configure time) that defines the array + count below from the app's
|
||||
// `uses_modules:` declaration in its app.md.
|
||||
//
|
||||
// Apps without uses_modules still get a stub array of length 0 — links cleanly.
|
||||
//
|
||||
// Framework reads via:
|
||||
//
|
||||
// for (size_t i = 0; i < fn::app_modules_count; ++i) {
|
||||
// const auto& m = fn::app_modules_array[i];
|
||||
// ImGui::Text("%s v%s — %s", m.name, m.version, m.description);
|
||||
// }
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace fn {
|
||||
|
||||
struct ModuleInfo {
|
||||
const char* name;
|
||||
const char* version;
|
||||
const char* description;
|
||||
};
|
||||
|
||||
extern const ModuleInfo app_modules_array[];
|
||||
extern const unsigned long app_modules_count;
|
||||
|
||||
} // namespace fn
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "core/app_about.h"
|
||||
|
||||
#include "app_base.h"
|
||||
#include "app_modules.h"
|
||||
#include "imgui.h"
|
||||
|
||||
#include <string>
|
||||
@@ -58,6 +60,40 @@ void about_window_render() {
|
||||
ImGui::TextWrapped("%s", g_description.c_str());
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// --- Framework version (issue 0097) ---
|
||||
ImGui::Text("Framework");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("v%s", fn::framework_version());
|
||||
|
||||
// --- Modules consumidos por la app (issue 0097) ---
|
||||
if (fn::app_modules_count > 0) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("Modules (%lu)", fn::app_modules_count);
|
||||
if (ImGui::BeginTable("##fn_modules_table", 2,
|
||||
ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerH |
|
||||
ImGuiTableFlags_SizingStretchProp)) {
|
||||
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 140.0f);
|
||||
ImGui::TableSetupColumn("Version", ImGuiTableColumnFlags_WidthFixed, 80.0f);
|
||||
for (unsigned long i = 0; i < fn::app_modules_count; ++i) {
|
||||
const auto& m = fn::app_modules_array[i];
|
||||
if (!m.name) continue;
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::TextUnformatted(m.name);
|
||||
if (m.description && *m.description && ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("%s", m.description);
|
||||
}
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::TextDisabled("v%s", m.version ? m.version : "?");
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("fn_registry");
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Promovido al registry desde cpp/apps/primitives_gallery/playground/tables/.
|
||||
// Ver issue 0081 + docs/TQL.md. Pure value types + enums.
|
||||
// Issue 0081-N: CellRenderer / ColumnSpec / BadgeRule / IconMapEntry (v1.1.0).
|
||||
// v1.4.0: ChipRule / ColorStop / CategoricalChip / ColorScale renderers.
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
@@ -131,16 +132,19 @@ enum class JoinStrategy { Left, Inner, Right, Full };
|
||||
// CellRenderer: declarative rendering mode per column (issue 0081-N, v1.1.0).
|
||||
// Phase 2 (issue 0081-O, v1.2.0): Button=5 added.
|
||||
// Phase 2.5 (issue 0081-O.5, v1.3.0): Dots=8 added (inline status timeline).
|
||||
// v1.4.0: CategoricalChip=9, ColorScale=10.
|
||||
// ----------------------------------------------------------------------------
|
||||
enum class CellRenderer : uint8_t {
|
||||
Text = 0, // default — current behavior
|
||||
Badge = 1, // colored badge per-value
|
||||
Progress = 2, // progress bar (0..1 or 0..100)
|
||||
Duration = 3, // milliseconds with color gradient
|
||||
Icon = 4, // icon lookup by value string
|
||||
Button = 5, // clickable button; emits TableEvent::ButtonClick
|
||||
Text = 0, // default — current behavior
|
||||
Badge = 1, // colored badge per-value
|
||||
Progress = 2, // progress bar (0..1 or 0..100)
|
||||
Duration = 3, // milliseconds with color gradient
|
||||
Icon = 4, // icon lookup by value string
|
||||
Button = 5, // clickable button; emits TableEvent::ButtonClick
|
||||
// 6, 7: reserved for Phase 3 (TextInput, Custom).
|
||||
Dots = 8, // inline dots sparkline; cell = separator-delimited tokens
|
||||
Dots = 8, // inline dots sparkline; cell = separator-delimited tokens
|
||||
CategoricalChip = 9, // filled circle (8px) to left of text; color by value match
|
||||
ColorScale = 10, // continuous N-color gradient tint on cell background
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -178,6 +182,19 @@ struct IconMapEntry {
|
||||
std::string color_hex; // optional; "" -> default text color
|
||||
};
|
||||
|
||||
// ChipRule: maps a cell value to a dot color for CategoricalChip renderer (v1.4.0).
|
||||
// If no rule matches, no dot is drawn (fallback: plain text only).
|
||||
struct ChipRule {
|
||||
std::string match; // exact match (case-sensitive) against cell value
|
||||
std::string color; // "#rrggbb" hex color for the filled circle
|
||||
};
|
||||
|
||||
// ColorStop: one stop in an N-color gradient for ColorScale renderer (v1.4.0).
|
||||
struct ColorStop {
|
||||
float position; // 0.0 (leftmost/min) to 1.0 (rightmost/max)
|
||||
std::string color; // "#rrggbb" hex color at this stop
|
||||
};
|
||||
|
||||
// ColumnSpec: rendering spec for one column. Indexed by column position.
|
||||
struct ColumnSpec {
|
||||
std::string id; // stable id, used in TQL
|
||||
@@ -215,6 +232,20 @@ struct ColumnSpec {
|
||||
float dots_glyph_size = 0.0f; // glyph size px; 0 = default font size
|
||||
int dots_max = 0; // hard limit on dots shown; 0 = no limit
|
||||
bool dots_show_count = false; // if true, appends " (N)" after dots
|
||||
|
||||
// CategoricalChip (v1.4.0): CellRenderer::CategoricalChip.
|
||||
// Draws a filled circle (radius ~4px) to the left of the cell text.
|
||||
// Color is determined by matching cell value against `chips` rules.
|
||||
// Always visible (not hover-only). If no rule matches, no dot is drawn.
|
||||
std::vector<ChipRule> chips; // value → color rules
|
||||
|
||||
// ColorScale (v1.4.0): CellRenderer::ColorScale.
|
||||
// Maps numeric cell value to a background tint via N-color gradient LERP.
|
||||
// Low alpha so text remains legible.
|
||||
double range_min = 0.0; // value at t=0.0
|
||||
double range_max = 1.0; // value at t=1.0
|
||||
float range_alpha = 0.25f; // [0..1]; background tint opacity
|
||||
std::vector<ColorStop> range_stops; // N≥2 stops; empty → default green→amber→red
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -292,6 +323,14 @@ struct State {
|
||||
// Caller-provided column_specs take precedence over aux_column_specs.
|
||||
std::vector<std::vector<ColumnSpec>> aux_column_specs;
|
||||
|
||||
// Per-table "Show UI" toggle. Moved from global UiCache to per-State so each
|
||||
// table's chrome (chips bar) can be toggled independently (issue: multiple
|
||||
// tables on screen, "Show UI" used to flip all at once).
|
||||
// Defaults: user_set=true + visible=false => chrome closed by default, ignoring
|
||||
// the API arg show_chrome from frame 1 (preserves legacy behavior).
|
||||
bool chrome_user_set = true;
|
||||
bool chrome_user_visible = false;
|
||||
|
||||
// Helpers (definidos en compute_stage.cpp).
|
||||
Stage& raw();
|
||||
const Stage& raw() const;
|
||||
|
||||
@@ -540,12 +540,14 @@ ApplyResult apply(const std::string& lua_text,
|
||||
lua_getfield(L, -1, "renderer");
|
||||
if (lua_isstring(L, -1)) {
|
||||
std::string rn = lua_tostring(L, -1);
|
||||
if (rn == "badge") cs.renderer = data_table::CellRenderer::Badge;
|
||||
else if (rn == "progress") cs.renderer = data_table::CellRenderer::Progress;
|
||||
else if (rn == "duration") cs.renderer = data_table::CellRenderer::Duration;
|
||||
else if (rn == "icon") cs.renderer = data_table::CellRenderer::Icon;
|
||||
else if (rn == "button") cs.renderer = data_table::CellRenderer::Button;
|
||||
else if (rn == "dots") cs.renderer = data_table::CellRenderer::Dots;
|
||||
if (rn == "badge") cs.renderer = data_table::CellRenderer::Badge;
|
||||
else if (rn == "progress") cs.renderer = data_table::CellRenderer::Progress;
|
||||
else if (rn == "duration") cs.renderer = data_table::CellRenderer::Duration;
|
||||
else if (rn == "icon") cs.renderer = data_table::CellRenderer::Icon;
|
||||
else if (rn == "button") cs.renderer = data_table::CellRenderer::Button;
|
||||
else if (rn == "dots") cs.renderer = data_table::CellRenderer::Dots;
|
||||
else if (rn == "categorical_chip") cs.renderer = data_table::CellRenderer::CategoricalChip;
|
||||
else if (rn == "color_scale") cs.renderer = data_table::CellRenderer::ColorScale;
|
||||
else cs.renderer = data_table::CellRenderer::Text;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
@@ -642,6 +644,57 @@ ApplyResult apply(const std::string& lua_text,
|
||||
if (lua_isnumber(L, -1)) cs.dots_glyph_size = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
// CategoricalChip (v1.4.0)
|
||||
lua_getfield(L, -1, "chips");
|
||||
if (lua_istable(L, -1)) {
|
||||
int nc = (int)lua_rawlen(L, -1);
|
||||
for (int j = 1; j <= nc; ++j) {
|
||||
lua_rawgeti(L, -1, j);
|
||||
if (lua_istable(L, -1)) {
|
||||
data_table::ChipRule cr;
|
||||
lua_getfield(L, -1, "match");
|
||||
if (lua_isstring(L, -1)) cr.match = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
lua_getfield(L, -1, "color");
|
||||
if (lua_isstring(L, -1)) cr.color = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
cs.chips.push_back(std::move(cr));
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1); // chips
|
||||
|
||||
// ColorScale (v1.4.0)
|
||||
lua_getfield(L, -1, "range_min");
|
||||
if (lua_isnumber(L, -1)) cs.range_min = (double)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
lua_getfield(L, -1, "range_max");
|
||||
if (lua_isnumber(L, -1)) cs.range_max = (double)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
lua_getfield(L, -1, "range_alpha");
|
||||
if (lua_isnumber(L, -1)) cs.range_alpha = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
lua_getfield(L, -1, "range_stops");
|
||||
if (lua_istable(L, -1)) {
|
||||
int ns = (int)lua_rawlen(L, -1);
|
||||
for (int j = 1; j <= ns; ++j) {
|
||||
lua_rawgeti(L, -1, j);
|
||||
if (lua_istable(L, -1)) {
|
||||
data_table::ColorStop stop;
|
||||
lua_getfield(L, -1, "position");
|
||||
if (lua_isnumber(L, -1)) stop.position = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
lua_getfield(L, -1, "color");
|
||||
if (lua_isstring(L, -1)) stop.color = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
cs.range_stops.push_back(std::move(stop));
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1); // range_stops
|
||||
|
||||
// Tooltip
|
||||
lua_getfield(L, -1, "tooltip");
|
||||
if (lua_isstring(L, -1)) cs.tooltip = lua_tostring(L, -1);
|
||||
|
||||
@@ -283,8 +283,7 @@ std::string emit(const State& state,
|
||||
// Emit the block only if at least one spec has a non-default renderer OR tooltip.
|
||||
bool any_renderable = false;
|
||||
for (const auto& cs : specs) {
|
||||
if (cs.renderer != data_table::CellRenderer::Text || cs.tooltip_on_hover ||
|
||||
cs.renderer == data_table::CellRenderer::Dots) {
|
||||
if (cs.renderer != data_table::CellRenderer::Text || cs.tooltip_on_hover) {
|
||||
any_renderable = true; break;
|
||||
}
|
||||
}
|
||||
@@ -297,12 +296,14 @@ std::string emit(const State& state,
|
||||
// renderer
|
||||
const char* rname = "text";
|
||||
switch (cs.renderer) {
|
||||
case data_table::CellRenderer::Badge: rname = "badge"; break;
|
||||
case data_table::CellRenderer::Progress: rname = "progress"; break;
|
||||
case data_table::CellRenderer::Duration: rname = "duration"; break;
|
||||
case data_table::CellRenderer::Icon: rname = "icon"; break;
|
||||
case data_table::CellRenderer::Button: rname = "button"; break;
|
||||
case data_table::CellRenderer::Dots: rname = "dots"; break;
|
||||
case data_table::CellRenderer::Badge: rname = "badge"; break;
|
||||
case data_table::CellRenderer::Progress: rname = "progress"; break;
|
||||
case data_table::CellRenderer::Duration: rname = "duration"; break;
|
||||
case data_table::CellRenderer::Icon: rname = "icon"; break;
|
||||
case data_table::CellRenderer::Button: rname = "button"; break;
|
||||
case data_table::CellRenderer::Dots: rname = "dots"; break;
|
||||
case data_table::CellRenderer::CategoricalChip: rname = "categorical_chip"; break;
|
||||
case data_table::CellRenderer::ColorScale: rname = "color_scale"; break;
|
||||
default: break;
|
||||
}
|
||||
out += ", renderer = " + lua_string_literal(rname);
|
||||
@@ -370,6 +371,41 @@ std::string emit(const State& state,
|
||||
out += std::string(", dots_glyph_size = ") + buf;
|
||||
}
|
||||
}
|
||||
// CategoricalChip (v1.4.0)
|
||||
if (cs.renderer == data_table::CellRenderer::CategoricalChip) {
|
||||
if (!cs.chips.empty()) {
|
||||
out += ", chips = {\n";
|
||||
for (const auto& cr : cs.chips) {
|
||||
out += " { match = " + lua_string_literal(cr.match);
|
||||
out += ", color = " + lua_string_literal(cr.color);
|
||||
out += " },\n";
|
||||
}
|
||||
out += " }";
|
||||
}
|
||||
}
|
||||
// ColorScale (v1.4.0)
|
||||
if (cs.renderer == data_table::CellRenderer::ColorScale) {
|
||||
char buf[64];
|
||||
std::snprintf(buf, sizeof(buf), "%g", cs.range_min);
|
||||
out += std::string(", range_min = ") + buf;
|
||||
std::snprintf(buf, sizeof(buf), "%g", cs.range_max);
|
||||
out += std::string(", range_max = ") + buf;
|
||||
if (cs.range_alpha != 0.25f) {
|
||||
std::snprintf(buf, sizeof(buf), "%g", (double)cs.range_alpha);
|
||||
out += std::string(", range_alpha = ") + buf;
|
||||
}
|
||||
if (!cs.range_stops.empty()) {
|
||||
out += ", range_stops = {\n";
|
||||
for (const auto& stop : cs.range_stops) {
|
||||
std::snprintf(buf, sizeof(buf), "%g", (double)stop.position);
|
||||
out += " { position = ";
|
||||
out += buf;
|
||||
out += ", color = " + lua_string_literal(stop.color);
|
||||
out += " },\n";
|
||||
}
|
||||
out += " }";
|
||||
}
|
||||
}
|
||||
// Tooltip
|
||||
if (cs.tooltip_on_hover) {
|
||||
out += ", tooltip = " + lua_string_literal(cs.tooltip.empty() ? "auto" : cs.tooltip);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,58 +0,0 @@
|
||||
#pragma once
|
||||
// data_table — render UI completa de tabla TQL.
|
||||
// Entry-point publica del stack data_table del registry.
|
||||
// Issue 0081-H. Promovido desde cpp/apps/primitives_gallery/playground/tables/data_table.h
|
||||
// Phase 2 (issue 0081-O, v1.2.0): Button renderer + event sink + tooltip + RightClick.
|
||||
//
|
||||
// Uso basico (back-compat, sin events):
|
||||
// data_table::State st; // persistir entre frames
|
||||
// ImGui::Begin("Window"); ImGui::BeginChild("tbl", {-1,-1});
|
||||
// data_table::render("my_table", {table1, table2}, st);
|
||||
// ImGui::EndChild(); ImGui::End();
|
||||
//
|
||||
// Uso con events (Phase 2):
|
||||
// std::vector<data_table::TableEvent> events;
|
||||
// data_table::render("my_table", {table1, table2}, st, &events);
|
||||
// for (auto& ev : events) {
|
||||
// if (ev.kind == data_table::TableEventKind::ButtonClick &&
|
||||
// ev.action_id == "cancel") { ... }
|
||||
// }
|
||||
//
|
||||
// Requiere ImGui context + ImPlot context activos.
|
||||
// Namespace identico al playground para facilitar migracion (solo cambiar include path).
|
||||
|
||||
#include "core/data_table_types.h"
|
||||
#include <vector>
|
||||
|
||||
namespace data_table {
|
||||
|
||||
// render — Render barra-de-chips + tabla + panels de visualizacion.
|
||||
// Mutates `st` en respuesta a la interaccion del usuario.
|
||||
//
|
||||
// `id` — ID unico de ImGui para esta instancia (ej. "##my_table").
|
||||
// `tables` — lista de TableInput. tables[0] es la main por defecto;
|
||||
// si State.main_source no-vacio se usa por nombre.
|
||||
// Tablas extra se exponen como joinables en la UI.
|
||||
// `st` — estado mutable. Debe persistir entre frames (no stack-local).
|
||||
// `events_out` — if non-null, populated with UI events (ButtonClick,
|
||||
// RowDoubleClick, RowRightClick) fired this frame. The caller
|
||||
// clears/reads the vector after each render call.
|
||||
// Pass nullptr to disable event collection (back-compat).
|
||||
// `show_chrome` — si false, oculta la barra de chips + breadcrumb por defecto.
|
||||
// El usuario puede reactivarla via el boton "Show UI".
|
||||
void render(const char* id,
|
||||
const std::vector<TableInput>& tables,
|
||||
State& st,
|
||||
std::vector<TableEvent>* events_out,
|
||||
bool show_chrome = true);
|
||||
|
||||
// Overload for back-compat: same as render(..., nullptr, show_chrome).
|
||||
inline void render(const char* id,
|
||||
const std::vector<TableInput>& tables,
|
||||
State& st,
|
||||
bool show_chrome = true)
|
||||
{
|
||||
render(id, tables, st, nullptr, show_chrome);
|
||||
}
|
||||
|
||||
} // namespace data_table
|
||||
@@ -1,200 +0,0 @@
|
||||
---
|
||||
name: data_table
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: viz
|
||||
version: "1.3.6"
|
||||
purity: impure
|
||||
signature: "void data_table::render(const char* id, const std::vector<TableInput>& tables, State& st, std::vector<TableEvent>* events_out = nullptr, bool show_chrome = true)"
|
||||
description: "Render UI completa de tabla TQL: chips bar, tabla, viz panels, column-stats inline, drill, color rules, joins, TQL editor, Ask AI, Button renderer, event sink (ButtonClick/RowDoubleClick/RowRightClick), tooltip per-cell, column_specs persisted in TQL. Dots renderer para sparkline-like de status (v1.3.0). Entry-point publica del stack data_table. Muta State segun interaccion del usuario."
|
||||
tags: [tables, viz, ui, imgui, tql, cpp-tables]
|
||||
uses_functions:
|
||||
- compute_stage_cpp_core
|
||||
- compute_pipeline_cpp_core
|
||||
- compute_column_stats_cpp_core
|
||||
- auto_detect_type_cpp_core
|
||||
- tql_emit_cpp_core
|
||||
- tql_apply_cpp_core
|
||||
- tql_helpers_cpp_core
|
||||
- tql_to_sql_cpp_core
|
||||
- lua_engine_cpp_core
|
||||
- join_tables_cpp_core
|
||||
- viz_render_cpp_viz
|
||||
uses_types:
|
||||
- data_table_types_cpp_core
|
||||
- ColumnSpec_cpp_core
|
||||
- CellRenderer_cpp_core
|
||||
- BadgeRule_cpp_core
|
||||
- IconMapEntry_cpp_core
|
||||
- TableInput_cpp_core
|
||||
- State_cpp_core
|
||||
- Stage_cpp_core
|
||||
- StageOutput_cpp_core
|
||||
- ViewMode_cpp_viz
|
||||
- ViewConfig_cpp_viz
|
||||
- VizPanel_cpp_viz
|
||||
- Join_cpp_core
|
||||
- Filter_cpp_core
|
||||
- DrillStep_cpp_core
|
||||
- DerivedColumn_cpp_core
|
||||
- Aggregation_cpp_core
|
||||
- SortClause_cpp_core
|
||||
- ColumnType_cpp_core
|
||||
- TableEvent_cpp_core
|
||||
- TableEventKind_cpp_core
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports:
|
||||
- imgui.h
|
||||
- app_base.h
|
||||
- core/data_table_types.h
|
||||
- core/lua_engine.h
|
||||
- core/tql_apply.h
|
||||
- core/tql_emit.h
|
||||
- core/tql_helpers.h
|
||||
- core/tql_to_sql.h
|
||||
- core/compute_stage.h
|
||||
- core/compute_pipeline.h
|
||||
- core/compute_column_stats.h
|
||||
- core/auto_detect_type.h
|
||||
- core/join_tables.h
|
||||
- viz/viz_render.h
|
||||
tested: true
|
||||
tests:
|
||||
- "back-compat: TableInput without column_specs does not crash"
|
||||
- "Badge: TableInput with Badge column_spec compiles and links"
|
||||
- "Progress: TableInput with Progress column_spec compiles and links"
|
||||
- "Duration: TableInput with Duration column_spec compiles and links"
|
||||
- "Icon: TableInput with Icon column_spec compiles and links"
|
||||
- "Button: TableEvent struct constructible; render() with events_out links"
|
||||
- "Tooltip: ColumnSpec with tooltip_on_hover=true compiles and links"
|
||||
- "Back-compat: both render() signatures (with/without events_out) link"
|
||||
- "Dots: ColumnSpec with CellRenderer::Dots + badges constructs correctly"
|
||||
- "Dots TQL roundtrip: State::aux_column_specs accepts Dots spec"
|
||||
test_file_path: "cpp/tests/test_column_specs.cpp"
|
||||
file_path: "cpp/functions/viz/data_table.cpp"
|
||||
params:
|
||||
- name: id
|
||||
desc: "ID unico ImGui para esta instancia, ej. '##orders_table'. Debe ser estable entre frames."
|
||||
- name: tables
|
||||
desc: "Lista de TableInput materializadas en memoria. tables[0] es la main por defecto; si State.main_source no-vacio se usa por nombre. Tablas extra se exponen como joinables en la UI de joins."
|
||||
- name: st
|
||||
desc: "Estado mutable completo: pipeline de stages, joins, viz config, ui tweaks, aux_column_specs (Phase 2). Debe persistir entre frames — no declarar en el stack del frame."
|
||||
- name: events_out
|
||||
desc: "Puntero a vector de TableEvent. Si non-null, se puebla con eventos de este frame (ButtonClick, RowDoubleClick, RowRightClick). El caller limpia/lee el vector despues de cada render. Pasar nullptr para desactivar (back-compat)."
|
||||
- name: show_chrome
|
||||
desc: "Si false, oculta chips bar + breadcrumb por defecto. El usuario puede reactivar con el boton 'Show UI'. El State persiste el override del usuario entre frames."
|
||||
output: "void. Muta st en respuesta a la interaccion del usuario (filtros, breakouts, sorts, drill, joins, viz mode). Los cambios son visibles en st al retornar. Events emitted via events_out."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```cpp
|
||||
#include "viz/data_table.h"
|
||||
#include "core/data_table_types.h"
|
||||
|
||||
// --- Setup (una vez) ---
|
||||
data_table::TableInput t;
|
||||
t.name = "orders";
|
||||
t.rows = num_rows;
|
||||
t.cols = num_cols;
|
||||
t.cells = cells_ptr; // row-major flat array, owner externo
|
||||
t.headers = {"id", "amount", "status", "actions"};
|
||||
t.types = {data_table::ColumnType::Int,
|
||||
data_table::ColumnType::Float,
|
||||
data_table::ColumnType::String,
|
||||
data_table::ColumnType::String};
|
||||
|
||||
// Phase 2: declarative renderers + tooltip
|
||||
t.column_specs.resize(4);
|
||||
t.column_specs[2].renderer = data_table::CellRenderer::Badge;
|
||||
t.column_specs[2].badges = {{"paid","#22c55e","Paid"},{"pending","#f59e0b",""}};
|
||||
t.column_specs[2].tooltip = "auto";
|
||||
t.column_specs[2].tooltip_on_hover = true;
|
||||
t.column_specs[3].renderer = data_table::CellRenderer::Button;
|
||||
t.column_specs[3].button_action = "cancel_order";
|
||||
t.column_specs[3].button_label = "Cancel";
|
||||
|
||||
data_table::State st; // persiste entre frames
|
||||
std::vector<data_table::TableEvent> events;
|
||||
|
||||
// --- Render (cada frame) ---
|
||||
ImGui::Begin("Orders");
|
||||
ImGui::BeginChild("##tbl", ImVec2(-1, -1));
|
||||
events.clear();
|
||||
data_table::render("##orders", {t}, st, &events);
|
||||
ImGui::EndChild();
|
||||
ImGui::End();
|
||||
|
||||
// --- Process events ---
|
||||
for (const auto& ev : events) {
|
||||
if (ev.kind == data_table::TableEventKind::ButtonClick &&
|
||||
ev.action_id == "cancel_order") {
|
||||
cancel_order(ev.row); // app handles the action
|
||||
}
|
||||
if (ev.kind == data_table::TableEventKind::RowDoubleClick) {
|
||||
open_order_detail(ev.row);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando una app necesita tabla con filtros + agregaciones + viz + joins sobre datos en memoria. Reemplaza `ImGui::BeginTable` inline + toda la logica TQL manual. Sustituye directamente el include del playground (`tables/data_table.h`) cambiando solo el path a `viz/data_table.h`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **ImGui + ImPlot context activos**: `render()` llama a APIs de ambas librerias. Llamar fuera de un frame activo causa UB.
|
||||
- **State no stack-local**: `State` contiene el historial de drill, pipeline de stages, cache de stats y buffers de UI. Declarar en el stack del frame reset todo el estado del usuario en cada frame.
|
||||
- **Drill-down propaga en State**: `st.active_stage` y `st.stages` se mutan por click en charts. El caller puede leer `st` tras `render()` para reaccionar.
|
||||
- **Thread-safety**: `render()` usa `static thread_local` para buffers intermedios. Llamar solo desde el main thread de ImGui.
|
||||
- **TableInput owner externo**: `cells` es un puntero raw al array del caller. Los datos deben sobrevivir durante toda la llamada a `render()`. No pasar puntero a vector que puede reallocarse.
|
||||
- **events_out no se limpia**: `render()` solo hace `push_back`. El caller debe llamar `events.clear()` antes de cada frame o acumulara eventos de frames anteriores.
|
||||
- **Button + celda vacia**: si el cell value es vacio, el boton NO se dibuja. La app controla cuando mostrar el boton poniendo un value no vacio (ej. "1" o el ID de la fila).
|
||||
- **RowRightClick emite evento Y abre popup interno**: la tabla de stages (stage>0) sigue abriendo su popup de drill. En el raw table (stage 0), se emite el evento pero el popup de drill antiguo tambien puede abrirse via `U.open_cell_popup`. El caller puede ignorar el popup interno y gestionar su propio menu al detectar `RowRightClick`.
|
||||
- **aux_column_specs merge**: si `TableInput.column_specs` esta vacio pero `State.aux_column_specs[0]` no, `render()` los aplica automaticamente. Si el caller pasa column_specs no vacios, ganan sobre los del State.
|
||||
- **Ask AI modal (llm_anthropic)**: el boton "Ask AI" usa un stub interno de `llm_anthropic` que retorna error por defecto. Para activar la feature real, compilar con `-DFN_LLM_ANTHROPIC=1` y proveer `infra/llm_anthropic.h` en el include path. Pendiente Wave 4: promover al registry.
|
||||
- **FN_TQL_DUCKDB**: modo SQL del Ask AI requiere compilar con `-DFN_TQL_DUCKDB=1` y la libreria DuckDB disponible.
|
||||
|
||||
## Notas
|
||||
|
||||
No hay tests unitarios directos: `render()` requiere ImGui + ImPlot context activos (imposible sin ventana GL). Cobertura via:
|
||||
1. `cpp/apps/primitives_gallery/playground/tables/` — playground original con self_test.cpp y e2e_run.sh.
|
||||
2. Wave 4: migration self-tests en las apps que migren desde el playground.
|
||||
|
||||
**Estado Wave 3.5 (issue 0081-I):**
|
||||
- Todos los includes del playground (`data_table_logic.h`, `tql.h`, `tql_to_sql.h`) eliminados. `data_table.cpp` compila sin el playground en el include path.
|
||||
- `tql::apply` firma extendida ya en `tql_apply_cpp_core` (wave anterior). Resuelto.
|
||||
- `tql_to_sql` promovido a `core/tql_to_sql.h`. Resuelto.
|
||||
- `data_table_logic` helpers (row_to_tsv, drill, view_mode, etc.) declarados como `static` en `data_table.cpp`. No son API pública.
|
||||
- `State::ensure_stage0/raw/active` implementados en `compute_stage.cpp`.
|
||||
- `ColStats` struct: usa el de `compute_column_stats_cpp_core`. Unificado.
|
||||
|
||||
**Deuda tecnica restante (Wave 4):**
|
||||
- `llm_anthropic` (Ask AI modal, issue 0080): stub interno activo. Promover a `cpp/functions/infra/llm_anthropic` para activar feature real.
|
||||
- `FN_TQL_DUCKDB`: modo SQL del Ask AI sin soporte en stub. Requiere DuckDB + flag de compilacion.
|
||||
- `column_specs` TQL roundtrip (Phase 2): actualmente caller-managed. No persisten en TQL emit/apply. Planificado en issue 0081-O.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
v1.1.0 (2026-05-15) — declarative CellRenderer (Badge/Progress/Duration/Icon) via TableInput.column_specs sidecar. Back-compat preservado: apps existentes sin column_specs siguen funcionando sin cambios.
|
||||
|
||||
v1.2.0 (2026-05-15) — Button renderer + event sink (ButtonClick/RowDoubleClick/RowRightClick) + tooltip per cell + column_specs persisted in TQL (aux_column_specs roundtrip). Back-compat preserved: events_out=nullptr by default; existing render() callers unchanged.
|
||||
|
||||
v1.3.0 (2026-05-15) — Dots renderer for inline status timelines (sparkline-like). Reuses badges for color mapping. dots_max/dots_separator/dots_show_count/dots_glyph_size fields. TQL roundtrip. dag_engine_ui canonical use case (10-col antipattern -> 6-col fix).
|
||||
|
||||
v1.3.1 (2026-05-15) — Dots renderer now draws filled circles via ImDrawList instead of Unicode glyph. Font-independent: works regardless of TTF glyph coverage. Closes "dots show as ?" bug in dag_engine_ui.
|
||||
|
||||
v1.3.2 (2026-05-15) — Hover dimming: row uses muted alpha (0.05 vs default 0.31); hovered cell gets a subtle overlay (~9% white) via ImDrawList. Badge no longer SpanAllColumns. Closes "table-wide bright highlight on hover".
|
||||
|
||||
v1.3.3 (2026-05-15) — Selectable bg disabled for Text/empty cells (was duplicating with manual overlay → gray double-hover). Explicit ImVec2 size on Selectable so empty cells get a hit area (fixes drag-select skipping empties). Single uniform hover layer across all cell renderers.
|
||||
|
||||
v1.3.4 (2026-05-15) — Row height tightened: GetTextLineHeight() (no spacing) replaces GetTextLineHeightWithSpacing() in Selectable size + manual overlay rect. Removes inflated row vertical padding introduced in v1.3.3.
|
||||
|
||||
v1.3.5 (2026-05-15) — Cell hover paints via TableSetBgColor (covers entire cell bg including CellPadding) instead of manual AddRectFilled inside content area. Hit-test expanded by CellPadding for proper edge-to-edge coverage. Closes "hover has gap between cell borders".
|
||||
|
||||
v1.3.6 (2026-05-15) — Selection (drag-range) also paints via TableSetBgColor — same edge-to-edge coverage as hover. Header/HeaderHovered/HeaderActive colors set to fully transparent so Selectable doesn't paint anything; all cell bg states (hover, selected, selected+hover) go through TableSetBgColor uniformly.
|
||||
|
||||
---
|
||||
Promovido desde `cpp/apps/primitives_gallery/playground/tables/data_table.{h,cpp}` — issue 0081-H.
|
||||
@@ -145,7 +145,7 @@ endif()
|
||||
|
||||
# --- Issue 0081-B — compute_stage + compute_pipeline (TQL pure logic) -------
|
||||
# tql_helpers.cpp added (issue 0081-I): compute_stage.cpp now delegates
|
||||
# aggregation_alias to tql_helpers to avoid ODR conflict in fn_table_viz lib.
|
||||
# aggregation_alias to tql_helpers to avoid ODR conflict in fn_module_data_table lib.
|
||||
add_fn_test(test_compute_stage test_compute_stage.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../functions/core/compute_stage.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../functions/core/tql_helpers.cpp)
|
||||
@@ -214,32 +214,32 @@ target_include_directories(tql_apply_test PRIVATE
|
||||
target_link_libraries(tql_apply_test PRIVATE lua54)
|
||||
add_test(NAME tql_apply_test COMMAND tql_apply_test)
|
||||
|
||||
# --- Issue 0081-I — fn_table_viz static lib smoke test ---------------------
|
||||
# Linker test: verifies that all 9 registry .cpp files in fn_table_viz resolve
|
||||
# --- Issue 0081-I — fn_module_data_table static lib smoke test ---------------------
|
||||
# Linker test: verifies that all 9 registry .cpp files in fn_module_data_table resolve
|
||||
# symbols correctly when linked as a static lib. Does NOT call data_table::render
|
||||
# (requires ImGui context + playground headers). Uses its own main().
|
||||
if(TARGET fn_table_viz)
|
||||
if(TARGET fn_module_data_table)
|
||||
add_executable(test_fn_table_viz_smoke
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/test_fn_table_viz_smoke.cpp)
|
||||
target_include_directories(test_fn_table_viz_smoke PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../functions
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../vendor/imgui
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../vendor/implot)
|
||||
target_link_libraries(test_fn_table_viz_smoke PRIVATE fn_table_viz)
|
||||
target_link_libraries(test_fn_table_viz_smoke PRIVATE fn_module_data_table)
|
||||
add_test(NAME test_fn_table_viz_smoke COMMAND test_fn_table_viz_smoke)
|
||||
endif()
|
||||
|
||||
# --- Issue 0081-N — declarative CellRenderer (Badge/Progress/Duration/Icon) --
|
||||
# Smoke + back-compat tests for TableInput.column_specs (v1.1.0).
|
||||
# Verifies type construction + link resolution; does NOT call render() (ImGui).
|
||||
if(TARGET fn_table_viz)
|
||||
if(TARGET fn_module_data_table)
|
||||
add_executable(test_column_specs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/test_column_specs.cpp)
|
||||
target_include_directories(test_column_specs PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../functions
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../vendor/imgui
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../vendor/implot)
|
||||
target_link_libraries(test_column_specs PRIVATE fn_table_viz)
|
||||
target_link_libraries(test_column_specs PRIVATE fn_module_data_table)
|
||||
add_test(NAME test_column_specs COMMAND test_column_specs)
|
||||
endif()
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// test_column_specs.cpp — Smoke / back-compat tests for declarative cell renderers.
|
||||
// Issue 0081-N, v1.1.0. Phase 2 (issue 0081-O, v1.2.0).
|
||||
// Phase 2.5 (issue 0081-O.5, v1.3.0): Dots renderer.
|
||||
// v1.4.0: CategoricalChip + ColorScale renderers (TestCategoricalChipRule,
|
||||
// TestColorScaleLerpTwoStops, TestColorScaleLerpThreeStops,
|
||||
// TestColorScaleOutOfRange).
|
||||
//
|
||||
// These tests verify:
|
||||
// 1. TableInput without column_specs compiles and links (back-compat).
|
||||
@@ -9,6 +12,11 @@
|
||||
// 7. Tooltip field: ColumnSpec with tooltip_on_hover=true compiles and links.
|
||||
// 8. render() overload with events_out=nullptr back-compat (symbol resolution only).
|
||||
// 9. Dots renderer: ColumnSpec with CellRenderer::Dots + badges constructs correctly.
|
||||
// 10. Dots TQL roundtrip: State::aux_column_specs accepts Dots spec.
|
||||
// 11. TestCategoricalChipRule: ChipRule with match="success" produces correct color.
|
||||
// 12. TestColorScaleLerpTwoStops: t=0→first color, t=1→last color, t=0.5→midpoint.
|
||||
// 13. TestColorScaleLerpThreeStops: t=0.25 lies between stop0 and stop1.
|
||||
// 14. TestColorScaleOutOfRange: t<0 saturates at first; t>1 saturates at last.
|
||||
//
|
||||
// None of these tests call data_table::render() (requires ImGui context).
|
||||
// They only verify that the new types are usable and that the symbols from
|
||||
@@ -18,7 +26,7 @@
|
||||
// Run: ./cpp/build/linux/tests/test_column_specs
|
||||
|
||||
#include "core/data_table_types.h"
|
||||
#include "viz/data_table.h"
|
||||
#include "data_table/data_table.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
@@ -396,6 +404,206 @@ static void test_dots_tql_roundtrip() {
|
||||
"(State::aux_column_specs accepts Dots spec)\n");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 11: TestCategoricalChipRule — ChipRule with match="success" correct color.
|
||||
// Verifies ChipRule struct construction + ColumnSpec.chips field accessible.
|
||||
// ---------------------------------------------------------------------------
|
||||
static void test_categorical_chip_rule() {
|
||||
ColumnSpec cs;
|
||||
cs.id = "state";
|
||||
cs.renderer = CellRenderer::CategoricalChip;
|
||||
cs.chips = {
|
||||
ChipRule{"success", "#22c55e"},
|
||||
ChipRule{"failure", "#ef4444"},
|
||||
ChipRule{"pending", "#f59e0b"},
|
||||
};
|
||||
|
||||
assert(cs.renderer == CellRenderer::CategoricalChip);
|
||||
assert(cs.chips.size() == 3);
|
||||
assert(cs.chips[0].match == "success");
|
||||
assert(cs.chips[0].color == "#22c55e");
|
||||
assert(cs.chips[1].match == "failure");
|
||||
assert(cs.chips[1].color == "#ef4444");
|
||||
assert(cs.chips[2].match == "pending");
|
||||
|
||||
// No matching rule for "unknown" — chips lookup returns nullptr (logic check).
|
||||
const ChipRule* found = nullptr;
|
||||
const char* test_val = "unknown";
|
||||
for (const auto& cr : cs.chips) {
|
||||
if (cr.match == test_val) { found = &cr; break; }
|
||||
}
|
||||
assert(found == nullptr && "no rule should match 'unknown'");
|
||||
|
||||
// Match "success" should find first rule.
|
||||
const ChipRule* found2 = nullptr;
|
||||
for (const auto& cr : cs.chips) {
|
||||
if (cr.match == std::string("success")) { found2 = &cr; break; }
|
||||
}
|
||||
assert(found2 != nullptr && found2->color == "#22c55e");
|
||||
|
||||
std::printf("PASS: TestCategoricalChipRule "
|
||||
"(3 chip rules, match/no-match logic correct)\n");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Headless color lerp helpers (mirrors the static functions in data_table.cpp,
|
||||
// replicated here so tests run without ImGui context).
|
||||
// Uses a plain struct RGB3 instead of std::tuple to avoid extra includes.
|
||||
// ---------------------------------------------------------------------------
|
||||
struct RGB3 { float r, g, b; };
|
||||
|
||||
static float lerp_f(float a, float b, float t) { return a + t * (b - a); }
|
||||
|
||||
// Parse "#rrggbb" -> RGB3 floats in [0,1]. Returns {-1,-1,-1} on failure.
|
||||
static RGB3 parse_rgb(const std::string& hex) {
|
||||
const char* p = hex.c_str();
|
||||
if (*p == '#') ++p;
|
||||
unsigned int r = 0, g = 0, b = 0;
|
||||
if (std::sscanf(p, "%02x%02x%02x", &r, &g, &b) != 3)
|
||||
return {-1.f, -1.f, -1.f};
|
||||
return {r / 255.f, g / 255.f, b / 255.f};
|
||||
}
|
||||
|
||||
// Lerp between two ColorStop RGB colors at a given global t.
|
||||
static RGB3 lerp_between(const ColorStop& lo, const ColorStop& hi, float t_global) {
|
||||
float span = hi.position - lo.position;
|
||||
float f = (span > 1e-6f) ? (t_global - lo.position) / span : 0.f;
|
||||
RGB3 ca = parse_rgb(lo.color);
|
||||
RGB3 cb = parse_rgb(hi.color);
|
||||
return {lerp_f(ca.r,cb.r,f), lerp_f(ca.g,cb.g,f), lerp_f(ca.b,cb.b,f)};
|
||||
}
|
||||
|
||||
// lerp_stops: full N-stop lerp (same logic as lerp_color_along_stops in data_table.cpp).
|
||||
static RGB3 lerp_stops(const std::vector<ColorStop>& stops, float t) {
|
||||
static const ColorStop kDefault[] = {
|
||||
{0.0f, "#22c55e"}, {0.5f, "#f59e0b"}, {1.0f, "#ef4444"}
|
||||
};
|
||||
static const int kDefaultN = 3;
|
||||
|
||||
// Build a working sorted copy.
|
||||
std::vector<ColorStop> s;
|
||||
if (stops.empty()) {
|
||||
for (int i = 0; i < kDefaultN; ++i) s.push_back(kDefault[i]);
|
||||
} else {
|
||||
s = stops;
|
||||
}
|
||||
// Simple insertion sort (N is tiny, avoids std::sort include).
|
||||
for (size_t i = 1; i < s.size(); ++i) {
|
||||
ColorStop key = s[i];
|
||||
int j = (int)i - 1;
|
||||
while (j >= 0 && s[j].position > key.position) { s[j+1] = s[j]; --j; }
|
||||
s[j+1] = key;
|
||||
}
|
||||
|
||||
t = t < 0.f ? 0.f : (t > 1.f ? 1.f : t);
|
||||
if (t <= s.front().position) return parse_rgb(s.front().color);
|
||||
if (t >= s.back().position) return parse_rgb(s.back().color);
|
||||
for (size_t i = 0; i + 1 < s.size(); ++i) {
|
||||
if (t >= s[i].position && t <= s[i+1].position)
|
||||
return lerp_between(s[i], s[i+1], t);
|
||||
}
|
||||
return parse_rgb(s.back().color);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 12: TestColorScaleLerpTwoStops — t=0→first, t=1→last, t=0.5→midpoint.
|
||||
// ---------------------------------------------------------------------------
|
||||
static void test_color_scale_lerp_two_stops() {
|
||||
std::vector<ColorStop> stops = {
|
||||
{0.0f, "#000000"}, // black
|
||||
{1.0f, "#ffffff"}, // white
|
||||
};
|
||||
ColumnSpec cs;
|
||||
cs.renderer = CellRenderer::ColorScale;
|
||||
cs.range_min = 0.0;
|
||||
cs.range_max = 1.0;
|
||||
cs.range_stops = stops;
|
||||
cs.range_alpha = 0.25f;
|
||||
|
||||
assert(cs.renderer == CellRenderer::ColorScale);
|
||||
assert(cs.range_stops.size() == 2);
|
||||
|
||||
// t=0.0 → black (0,0,0)
|
||||
RGB3 c0 = lerp_stops(stops, 0.0f);
|
||||
assert(c0.r < 0.01f && c0.g < 0.01f && c0.b < 0.01f);
|
||||
|
||||
// t=1.0 → white (1,1,1)
|
||||
RGB3 c1 = lerp_stops(stops, 1.0f);
|
||||
assert(c1.r > 0.99f && c1.g > 0.99f && c1.b > 0.99f);
|
||||
|
||||
// t=0.5 → midpoint (0.5, 0.5, 0.5) within floating-point tolerance
|
||||
RGB3 c5 = lerp_stops(stops, 0.5f);
|
||||
assert(c5.r > 0.49f && c5.r < 0.51f);
|
||||
assert(c5.g > 0.49f && c5.g < 0.51f);
|
||||
assert(c5.b > 0.49f && c5.b < 0.51f);
|
||||
|
||||
std::printf("PASS: TestColorScaleLerpTwoStops "
|
||||
"(t=0→black, t=1→white, t=0.5→mid-grey)\n");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 13: TestColorScaleLerpThreeStops — t=0.25 between stop0 and stop1.
|
||||
// Stops: {0.0,red}, {0.5,green}, {1.0,blue}.
|
||||
// At t=0.25 we expect halfway between red and green.
|
||||
// ---------------------------------------------------------------------------
|
||||
static void test_color_scale_lerp_three_stops() {
|
||||
// red=#ff0000, green=#00ff00, blue=#0000ff
|
||||
std::vector<ColorStop> stops = {
|
||||
{0.0f, "#ff0000"}, // red
|
||||
{0.5f, "#00ff00"}, // green
|
||||
{1.0f, "#0000ff"}, // blue
|
||||
};
|
||||
|
||||
// t=0.25 is halfway between stop0 (t=0) and stop1 (t=0.5).
|
||||
// Lerp factor f = (0.25 - 0.0) / (0.5 - 0.0) = 0.5.
|
||||
// Expected: R = lerp(1,0,0.5)=0.5, G = lerp(0,1,0.5)=0.5, B = lerp(0,0,0.5)=0.
|
||||
RGB3 ca = lerp_stops(stops, 0.25f);
|
||||
assert(ca.r > 0.49f && ca.r < 0.51f && "R should be ~0.5 at t=0.25");
|
||||
assert(ca.g > 0.49f && ca.g < 0.51f && "G should be ~0.5 at t=0.25");
|
||||
assert(ca.b < 0.01f && "B should be ~0 at t=0.25");
|
||||
|
||||
// t=0.75 is halfway between stop1 (t=0.5) and stop2 (t=1.0).
|
||||
// Expected: R=0, G=0.5, B=0.5.
|
||||
RGB3 cb = lerp_stops(stops, 0.75f);
|
||||
assert(cb.r < 0.01f && "R should be ~0 at t=0.75");
|
||||
assert(cb.g > 0.49f && cb.g < 0.51f && "G should be ~0.5 at t=0.75");
|
||||
assert(cb.b > 0.49f && cb.b < 0.51f && "B should be ~0.5 at t=0.75");
|
||||
|
||||
std::printf("PASS: TestColorScaleLerpThreeStops "
|
||||
"(t=0.25 between stop0/stop1, t=0.75 between stop1/stop2)\n");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 14: TestColorScaleOutOfRange — t<0 saturates at first; t>1 at last.
|
||||
// ---------------------------------------------------------------------------
|
||||
static void test_color_scale_out_of_range() {
|
||||
std::vector<ColorStop> stops = {
|
||||
{0.0f, "#ff0000"}, // red at t=0
|
||||
{1.0f, "#0000ff"}, // blue at t=1
|
||||
};
|
||||
|
||||
// t=-0.5 → clamp to 0 → red
|
||||
RGB3 cu = lerp_stops(stops, -0.5f);
|
||||
assert(cu.r > 0.99f && "under-range should saturate at first stop (red)");
|
||||
assert(cu.b < 0.01f);
|
||||
|
||||
// t=1.5 → clamp to 1 → blue
|
||||
RGB3 co = lerp_stops(stops, 1.5f);
|
||||
assert(co.r < 0.01f && "over-range should saturate at last stop (blue)");
|
||||
assert(co.b > 0.99f);
|
||||
|
||||
// ColumnSpec struct fields accessible and defaults sensible.
|
||||
ColumnSpec cs;
|
||||
cs.renderer = CellRenderer::ColorScale;
|
||||
cs.range_min = -10.0;
|
||||
cs.range_max = 10.0;
|
||||
assert(cs.range_alpha == 0.25f && "default range_alpha should be 0.25");
|
||||
assert(cs.range_stops.empty() && "default range_stops should be empty (→ use default gradient)");
|
||||
|
||||
std::printf("PASS: TestColorScaleOutOfRange "
|
||||
"(t<0 saturates at first stop, t>1 saturates at last stop)\n");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// main
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -411,6 +619,10 @@ int main() {
|
||||
test_render_backcompat_overload();
|
||||
test_dots_column_spec();
|
||||
test_dots_tql_roundtrip();
|
||||
std::printf("=== ALL TESTS PASSED (10/10) ===\n");
|
||||
test_categorical_chip_rule();
|
||||
test_color_scale_lerp_two_stops();
|
||||
test_color_scale_lerp_three_stops();
|
||||
test_color_scale_out_of_range();
|
||||
std::printf("=== ALL TESTS PASSED (14/14) ===\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#include "core/auto_detect_type.h"
|
||||
#include "core/compute_column_stats.h"
|
||||
#include "viz/viz_render.h"
|
||||
#include "viz/data_table.h"
|
||||
#include "data_table/data_table.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
|
||||
Reference in New Issue
Block a user