d6bdab89e8
- Nuevo chart_panel_begin/end local (reemplaza dashboard_panel con AutoResizeY): BeginChild con altura explicita pasada por parametro, NoScrollbar | NoScrollWithMouse. Rompe el feedback loop plot <-> panel que causaba deslizamiento lateral y scrollbar fugaz. - Altura de charts fija 260px (antes GetContentRegionAvail().y * 0.35). Sin esto, redimensionar la ventana propaga cambios de altura a todos los plots y se ve vibracion. - KPIs reorganizados en ImGui::BeginTable 4 cols x 2 rows. Las celdas de tabla propagan ancho constrained, necesario para que el BeginChild interno del kpi_card (card v1.2 compacta 78px) ocupe exactamente la celda. - imgui.ini al .gitignore: estado local de la ventana, no versionable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
295 lines
11 KiB
C++
295 lines
11 KiB
C++
#include "views.h"
|
|
#include "imgui.h"
|
|
#include "implot.h"
|
|
|
|
#include "viz/kpi_card.h"
|
|
#include "viz/bar_chart.h"
|
|
#include "viz/pie_chart.h"
|
|
#include "viz/table_view.h"
|
|
#include "viz/sparkline.h"
|
|
#include "core/dashboard_panel.h"
|
|
#include "core/dashboard_grid.h"
|
|
#include "core/fps_overlay.h"
|
|
#include "core/fullscreen_window.h"
|
|
#include "core/tokens.h"
|
|
#include "core/page_header.h"
|
|
#include "core/empty_state.h"
|
|
#include "core/badge.h"
|
|
|
|
#include <cstdio>
|
|
#include <vector>
|
|
|
|
static std::vector<const char*> to_cstr(const std::vector<std::string>& v) {
|
|
std::vector<const char*> out;
|
|
out.reserve(v.size());
|
|
for (auto& s : v) out.push_back(s.c_str());
|
|
return out;
|
|
}
|
|
|
|
void draw_kpi_row(const RegistryStats& stats) {
|
|
float tested_pct = stats.total_functions > 0
|
|
? 100.0f * stats.tested_functions / stats.total_functions : 0.0f;
|
|
float pure_pct = stats.total_functions > 0
|
|
? 100.0f * stats.pure_functions / stats.total_functions : 0.0f;
|
|
|
|
// ImGui::BeginTable da celdas con ancho constrained, algo que BeginGroup
|
|
// (dashboard_grid) no hace — necesario para que el BeginChild dentro de
|
|
// kpi_card ocupe exactamente la celda y no desborde.
|
|
const ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame
|
|
| ImGuiTableFlags_NoPadOuterX;
|
|
|
|
if (ImGui::BeginTable("##kpi_grid", 4, flags)) {
|
|
struct KPI { const char* label; float value; const char* fmt; };
|
|
const KPI cards[8] = {
|
|
{"Functions", static_cast<float>(stats.total_functions), "%.0f"},
|
|
{"Types", static_cast<float>(stats.total_types), "%.0f"},
|
|
{"Apps", static_cast<float>(stats.total_apps), "%.0f"},
|
|
{"Analysis", static_cast<float>(stats.total_analysis), "%.0f"},
|
|
{"Unit Tests", static_cast<float>(stats.total_unit_tests), "%.0f"},
|
|
{"Proposals", static_cast<float>(stats.total_proposals), "%.0f"},
|
|
{"Tested", tested_pct, "%.0f%%"},
|
|
{"Pure", pure_pct, "%.0f%%"},
|
|
};
|
|
for (int i = 0; i < 8; i++) {
|
|
if (i % 4 == 0) ImGui::TableNextRow();
|
|
ImGui::TableSetColumnIndex(i % 4);
|
|
kpi_card(cards[i].label, cards[i].value, 0.0f, nullptr, 0, cards[i].fmt);
|
|
}
|
|
ImGui::EndTable();
|
|
}
|
|
}
|
|
|
|
// Chart panel con tamano FIJO (no AutoResizeY) para evitar el feedback loop
|
|
// con ImPlot que provocaba deslizamiento lateral de las barras y scrollbar
|
|
// intermitente. Usa los mismos tokens que dashboard_panel para consistencia
|
|
// visual, pero con tamano determinista.
|
|
static bool chart_panel_begin(const char* title, const ImVec2& size) {
|
|
using namespace fn_tokens;
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, colors::surface);
|
|
ImGui::PushStyleColor(ImGuiCol_Border, colors::border);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, radius::md);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::md, spacing::md));
|
|
|
|
ImGui::BeginChild(title, size, ImGuiChildFlags_Borders,
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
|
|
ImGui::TextUnformatted(title);
|
|
ImGui::PopStyleColor();
|
|
ImGui::Separator();
|
|
return true;
|
|
}
|
|
|
|
static void chart_panel_end() {
|
|
ImGui::EndChild();
|
|
ImGui::PopStyleVar(3);
|
|
ImGui::PopStyleColor(2);
|
|
}
|
|
|
|
void draw_charts(RegistryData& data, float height) {
|
|
// ImGui::BeginTable para reparto equitativo de ancho en 4 columnas y que
|
|
// cada chart_panel tenga ancho constrained via GetContentRegionAvail.
|
|
const ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame
|
|
| ImGuiTableFlags_NoPadOuterX;
|
|
|
|
// Altura util dentro del panel (height total - title row - separator - padding).
|
|
// El plot recibe exactamente esto, asi que no hay redimensionado recursivo.
|
|
const float plot_h = height - 48.0f;
|
|
|
|
if (ImGui::BeginTable("##chart_grid", 4, flags)) {
|
|
ImGui::TableNextRow();
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
|
{
|
|
ImVec2 sz(ImGui::GetContentRegionAvail().x, height);
|
|
chart_panel_begin("By Language", sz);
|
|
auto labels = to_cstr(data.lang_labels);
|
|
if (!labels.empty())
|
|
bar_chart("##lang", labels.data(), data.lang_values.data(),
|
|
static_cast<int>(labels.size()), 0.67f, plot_h);
|
|
chart_panel_end();
|
|
}
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
{
|
|
ImVec2 sz(ImGui::GetContentRegionAvail().x, height);
|
|
chart_panel_begin("By Domain", sz);
|
|
auto labels = to_cstr(data.domain_labels);
|
|
if (!labels.empty())
|
|
bar_chart("##domain", labels.data(), data.domain_values.data(),
|
|
static_cast<int>(labels.size()), 0.67f, plot_h);
|
|
chart_panel_end();
|
|
}
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
|
{
|
|
ImVec2 sz(ImGui::GetContentRegionAvail().x, height);
|
|
chart_panel_begin("Purity", sz);
|
|
const char* labels[] = {"Pure", "Impure"};
|
|
float values[] = {static_cast<float>(data.stats.pure_functions),
|
|
static_cast<float>(data.stats.impure_functions)};
|
|
pie_chart("##purity", labels, values, 2);
|
|
chart_panel_end();
|
|
}
|
|
|
|
ImGui::TableSetColumnIndex(3);
|
|
{
|
|
ImVec2 sz(ImGui::GetContentRegionAvail().x, height);
|
|
chart_panel_begin("Kind", sz);
|
|
auto labels = to_cstr(data.kind_labels);
|
|
if (!labels.empty())
|
|
pie_chart("##kind", labels.data(), data.kind_values.data(),
|
|
static_cast<int>(labels.size()));
|
|
chart_panel_end();
|
|
}
|
|
|
|
ImGui::EndTable();
|
|
}
|
|
}
|
|
|
|
void draw_recent_functions(const std::vector<FunctionRow>& funcs) {
|
|
if (funcs.empty()) {
|
|
empty_state("( no data )", "No functions yet",
|
|
"Run 'fn index' to populate the registry");
|
|
return;
|
|
}
|
|
const char* headers[] = {"Name", "Lang", "Domain", "Kind", "Purity", "Tested", "Created"};
|
|
constexpr int cols = 7;
|
|
std::vector<std::string> cell_strings;
|
|
cell_strings.reserve(funcs.size() * cols);
|
|
for (auto& f : funcs) {
|
|
cell_strings.push_back(f.name);
|
|
cell_strings.push_back(f.lang);
|
|
cell_strings.push_back(f.domain);
|
|
cell_strings.push_back(f.kind);
|
|
cell_strings.push_back(f.purity);
|
|
cell_strings.push_back(f.tested ? "yes" : "no");
|
|
cell_strings.push_back(f.created_at.substr(0, 10));
|
|
}
|
|
auto cells = to_cstr(cell_strings);
|
|
table_view("##recent", headers, cols, cells.data(), static_cast<int>(funcs.size()));
|
|
}
|
|
|
|
void draw_apps_list(const std::vector<AppRow>& apps) {
|
|
if (apps.empty()) {
|
|
empty_state("( no data )", "No apps registered",
|
|
"Clone apps with 'fn app clone <id>' or run 'fn sync'");
|
|
return;
|
|
}
|
|
const char* headers[] = {"Name", "Lang", "Domain", "Framework", "Description"};
|
|
constexpr int cols = 5;
|
|
std::vector<std::string> cell_strings;
|
|
cell_strings.reserve(apps.size() * cols);
|
|
for (auto& a : apps) {
|
|
cell_strings.push_back(a.name);
|
|
cell_strings.push_back(a.lang);
|
|
cell_strings.push_back(a.domain);
|
|
cell_strings.push_back(a.framework);
|
|
cell_strings.push_back(a.description);
|
|
}
|
|
auto cells = to_cstr(cell_strings);
|
|
table_view("##apps", headers, cols, cells.data(), static_cast<int>(apps.size()));
|
|
}
|
|
|
|
void draw_analysis_list(const std::vector<AnalysisRow>& analyses) {
|
|
if (analyses.empty()) {
|
|
empty_state("( no data )", "No analysis yet",
|
|
"Create one with 'fn run init_jupyter_analysis <name>'");
|
|
return;
|
|
}
|
|
const char* headers[] = {"Name", "Domain", "Description"};
|
|
constexpr int cols = 3;
|
|
std::vector<std::string> cell_strings;
|
|
cell_strings.reserve(analyses.size() * cols);
|
|
for (auto& a : analyses) {
|
|
cell_strings.push_back(a.name);
|
|
cell_strings.push_back(a.domain);
|
|
cell_strings.push_back(a.description);
|
|
}
|
|
auto cells = to_cstr(cell_strings);
|
|
table_view("##analysis", headers, cols, cells.data(), static_cast<int>(analyses.size()));
|
|
}
|
|
|
|
void draw_types_list(const std::vector<TypeRow>& types) {
|
|
if (types.empty()) {
|
|
empty_state("( no data )", "No types yet",
|
|
"Types are indexed from the registry alongside functions");
|
|
return;
|
|
}
|
|
const char* headers[] = {"Name", "Lang", "Domain", "Algebraic", "Description"};
|
|
constexpr int cols = 5;
|
|
std::vector<std::string> cell_strings;
|
|
cell_strings.reserve(types.size() * cols);
|
|
for (auto& t : types) {
|
|
cell_strings.push_back(t.name);
|
|
cell_strings.push_back(t.lang);
|
|
cell_strings.push_back(t.domain);
|
|
cell_strings.push_back(t.algebraic);
|
|
cell_strings.push_back(t.description);
|
|
}
|
|
auto cells = to_cstr(cell_strings);
|
|
table_view("##types", headers, cols, cells.data(), static_cast<int>(types.size()));
|
|
}
|
|
|
|
void draw_dashboard(RegistryData& data) {
|
|
// Aplicar tema una sola vez por vida de la app.
|
|
static bool theme_applied = false;
|
|
if (!theme_applied) {
|
|
fn_tokens::apply_dark_theme();
|
|
theme_applied = true;
|
|
}
|
|
|
|
fps_overlay();
|
|
fullscreen_window_begin("##dashboard");
|
|
|
|
// Subtitle con conteos — contexto rápido para el usuario
|
|
char subtitle[128];
|
|
std::snprintf(subtitle, sizeof(subtitle),
|
|
"%d functions · %d types · %d apps · %d analyses",
|
|
data.stats.total_functions, data.stats.total_types,
|
|
data.stats.total_apps, data.stats.total_analysis);
|
|
|
|
// Header con acción Reload a la derecha
|
|
page_header_begin("fn_registry Dashboard", subtitle);
|
|
ImGui::SameLine(ImGui::GetWindowWidth() - 120.0f);
|
|
if (ImGui::Button("Reload")) {
|
|
ImGui::GetIO().UserData = reinterpret_cast<void*>(1);
|
|
}
|
|
page_header_end();
|
|
|
|
// KPIs
|
|
draw_kpi_row(data.stats);
|
|
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
|
|
|
|
// Charts — altura FIJA en pixeles (no depende del resize de la ventana).
|
|
// Antes usabamos remaining*0.35, pero eso recalculaba todo el layout al
|
|
// redimensionar, provocando vibracion visible en los plots.
|
|
constexpr float chart_h = 260.0f;
|
|
draw_charts(data, chart_h);
|
|
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
|
|
|
|
// Tables
|
|
if (ImGui::BeginTabBar("##tables")) {
|
|
if (ImGui::BeginTabItem("Recent Functions")) {
|
|
draw_recent_functions(data.recent_funcs);
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Apps")) {
|
|
draw_apps_list(data.apps);
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Analysis")) {
|
|
draw_analysis_list(data.analyses);
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Types")) {
|
|
draw_types_list(data.types);
|
|
ImGui::EndTabItem();
|
|
}
|
|
ImGui::EndTabBar();
|
|
}
|
|
|
|
fullscreen_window_end();
|
|
}
|