#include "views.h" #include "viz/graph_types.h" #include "viz/graph_viewport.h" #include "viz/graph_sources.h" #include "core/button.h" #include "core/icon_button.h" #include "core/toolbar.h" #include "core/select.h" #include "core/modal_dialog.h" #include "core/text_input.h" #include "core/tokens.h" #include "core/icons_tabler.h" #include "imgui.h" #include #include namespace ge { namespace { const char* k_layout_names[] = { "force", "grid", "circular", "radial", "hierarchical", "fixed", }; constexpr int k_layout_count = (int)(sizeof(k_layout_names) / sizeof(k_layout_names[0])); ImVec4 abgr_to_imvec4(uint32_t c) { uint8_t r = (uint8_t)( c & 0xFF); uint8_t g = (uint8_t)((c >> 8) & 0xFF); uint8_t b = (uint8_t)((c >> 16) & 0xFF); uint8_t a = (uint8_t)((c >> 24) & 0xFF); return ImVec4(r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f); } void color_swatch(uint32_t color, float size = 12.0f) { ImVec2 p = ImGui::GetCursorScreenPos(); ImDrawList* dl = ImGui::GetWindowDrawList(); dl->AddRectFilled(p, ImVec2(p.x + size, p.y + size), ImGui::ColorConvertFloat4ToU32(abgr_to_imvec4(color)), 3.0f); ImGui::Dummy(ImVec2(size, size)); ImGui::SameLine(); } } // namespace void views_reset_visibility(AppState& app) { if (!app.graph) return; int nt = app.graph->type_count; int nr = app.graph->rel_type_count; if (nt > 256) nt = 256; if (nr > 256) nr = 256; for (int i = 0; i < nt; ++i) app.type_visible[i] = true; for (int i = 0; i < nr; ++i) app.rel_type_visible[i] = true; app.type_visible_n = nt; app.rel_type_visible_n = nr; } void views_apply_visibility(AppState& app) { if (!app.graph) return; GraphData& g = *app.graph; for (int i = 0; i < g.node_count; ++i) { uint16_t t = g.nodes[i].type_id; bool vis = (t < (uint16_t)app.type_visible_n) ? app.type_visible[t] : true; if (vis) g.nodes[i].flags |= NF_VISIBLE; else g.nodes[i].flags &= ~NF_VISIBLE; } for (int i = 0; i < g.edge_count; ++i) { const GraphEdge& e = g.edges[i]; bool rel_vis = (e.type_id < (uint16_t)app.rel_type_visible_n) ? app.rel_type_visible[e.type_id] : true; // Si los endpoints estan ocultos, la arista tambien. bool src_vis = (e.source < (uint32_t)g.node_count) && (g.nodes[e.source].flags & NF_VISIBLE); bool tgt_vis = (e.target < (uint32_t)g.node_count) && (g.nodes[e.target].flags & NF_VISIBLE); bool vis = rel_vis && src_vis && tgt_vis; if (vis) g.edges[i].flags |= EF_VISIBLE; else g.edges[i].flags &= ~EF_VISIBLE; } } // ---------------------------------------------------------------------------- // Toolbar // ---------------------------------------------------------------------------- void views_toolbar(AppState& app) { using namespace fn_ui; toolbar_begin(); if (button(TI_FOLDER " Open file...", ButtonVariant::Secondary)) { app.show_open_modal = true; } toolbar_separator(); ImGui::TextUnformatted("Layout:"); ImGui::SameLine(); ImGui::SetNextItemWidth(140); int idx = app.layout_mode; if (ImGui::Combo("##layout", &idx, k_layout_names, k_layout_count)) { if (idx != app.layout_mode) { app.layout_mode = idx; ++app.apply_layout_tick; } } toolbar_separator(); if (button(TI_FILTER " Filters...", ButtonVariant::Subtle)) { app.show_filters_modal = true; } if (button(TI_ARROWS_MAXIMIZE " Fit view", ButtonVariant::Subtle)) { app.want_fit = true; } if (button(TI_DEVICE_FLOPPY " Save layout", ButtonVariant::Subtle)) { app.want_save_layout = true; } if (button(TI_REFRESH " Reload", ButtonVariant::Subtle)) { app.want_reload = true; } toolbar_separator(); ImGui::Checkbox("GPU layout", &app.use_gpu); ImGui::SameLine(); ImGui::Checkbox("Labels", &app.labels_enabled); if (app.viewport) { ImGui::SameLine(); ImGui::Checkbox("Run layout", &app.viewport->layout_running); } toolbar_end(); } // ---------------------------------------------------------------------------- // Legend // ---------------------------------------------------------------------------- void views_legend(AppState& app) { if (!app.panel_legend) return; if (!ImGui::Begin("Legend", &app.panel_legend)) { ImGui::End(); return; } if (!app.graph) { ImGui::TextUnformatted("(no graph loaded)"); ImGui::End(); return; } GraphData& g = *app.graph; bool changed = false; ImGui::TextUnformatted("Entity types"); ImGui::Separator(); for (int i = 0; i < g.type_count && i < app.type_visible_n; ++i) { const EntityType& et = g.types[i]; color_swatch(et.color); char id[32]; std::snprintf(id, sizeof(id), "##t%d", i); bool v = app.type_visible[i]; if (ImGui::Checkbox(id, &v)) { app.type_visible[i] = v; changed = true; } ImGui::SameLine(); ImGui::TextUnformatted(et.name ? et.name : "(unnamed)"); } if (g.rel_type_count > 0) { ImGui::Spacing(); ImGui::TextUnformatted("Relation types"); ImGui::Separator(); for (int i = 0; i < g.rel_type_count && i < app.rel_type_visible_n; ++i) { const RelationType& rt = g.rel_types[i]; color_swatch(rt.color); char id[32]; std::snprintf(id, sizeof(id), "##r%d", i); bool v = app.rel_type_visible[i]; if (ImGui::Checkbox(id, &v)) { app.rel_type_visible[i] = v; changed = true; } ImGui::SameLine(); ImGui::TextUnformatted(rt.name ? rt.name : "(unnamed)"); } } if (changed) views_apply_visibility(app); ImGui::End(); } // ---------------------------------------------------------------------------- // Inspector // ---------------------------------------------------------------------------- void views_inspector(AppState& app) { if (!app.panel_inspector) return; if (!ImGui::Begin("Inspector", &app.panel_inspector)) { ImGui::End(); return; } if (!app.graph || !app.viewport) { ImGui::TextUnformatted("(no graph)"); ImGui::End(); return; } GraphData& g = *app.graph; const auto& sel = app.viewport->selection; if (sel.empty()) { ImGui::TextUnformatted("No selection."); ImGui::TextWrapped("Click a node, or shift+drag to lasso a region."); ImGui::End(); return; } if (sel.size() > 1) { ImGui::Text("%zu nodes selected", sel.size()); ImGui::Separator(); for (size_t i = 0; i < sel.size() && i < 32; ++i) { int idx = sel[i]; if (idx < 0 || idx >= g.node_count) continue; const GraphNode& n = g.nodes[idx]; const char* lbl = graph::graph_label(&g, n.label_idx); ImGui::BulletText("[%d] %s", idx, lbl && *lbl ? lbl : "(unnamed)"); } if (sel.size() > 32) ImGui::TextDisabled("(...%zu more)", sel.size() - 32); ImGui::End(); return; } int idx = sel.front(); if (idx < 0 || idx >= g.node_count) { ImGui::TextUnformatted("(invalid selection)"); ImGui::End(); return; } const GraphNode& n = g.nodes[idx]; const char* lbl = graph::graph_label(&g, n.label_idx); const char* tname = (n.type_id < (uint16_t)g.type_count && g.types[n.type_id].name) ? g.types[n.type_id].name : "(no-type)"; ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); ImGui::TextUnformatted("label:"); ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::TextUnformatted(lbl && *lbl ? lbl : "(none)"); ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); ImGui::TextUnformatted("type:"); ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::TextUnformatted(tname); ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); ImGui::Text("idx=%d user_data=%llx pos=(%.1f, %.1f)", idx, (unsigned long long)n.user_data, n.x, n.y); ImGui::PopStyleColor(); ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); ImGui::TextUnformatted("Neighbors:"); ImGui::PopStyleColor(); int neighbor_count = 0; for (int e = 0; e < g.edge_count && neighbor_count < 64; ++e) { const GraphEdge& edge = g.edges[e]; int other = -1; if (edge.source == (uint32_t)idx) other = (int)edge.target; else if (edge.target == (uint32_t)idx) other = (int)edge.source; if (other < 0 || other >= g.node_count) continue; const char* olbl = graph::graph_label(&g, g.nodes[other].label_idx); char buf[256]; std::snprintf(buf, sizeof(buf), "[%d] %s", other, olbl && *olbl ? olbl : "(unnamed)"); if (ImGui::Selectable(buf)) { graph_viewport_clear_selection(g, *app.viewport); graph_viewport_add_to_selection(g, *app.viewport, other); } ++neighbor_count; } if (neighbor_count == 0) ImGui::TextDisabled("(none)"); ImGui::End(); } // ---------------------------------------------------------------------------- // Stats // ---------------------------------------------------------------------------- void views_stats(AppState& app) { if (!app.panel_stats) return; if (!ImGui::Begin("Stats", &app.panel_stats)) { ImGui::End(); return; } if (!app.graph || !app.viewport) { ImGui::TextUnformatted("(no graph)"); ImGui::End(); return; } int sel = (int)app.viewport->selection.size(); ImGui::Text("nodes=%d edges=%d types=%d rel_types=%d", app.graph->node_count, app.graph->edge_count, app.graph->type_count, app.graph->rel_type_count); ImGui::Text("fps=%d energy=%.4f selection=%d", app.fps_estimate, app.viewport->layout_energy, sel); ImGui::Text("layout=%s mode=%s", k_layout_names[app.layout_mode % k_layout_count], app.use_gpu ? "GPU" : "CPU"); ImGui::End(); } // ---------------------------------------------------------------------------- // Modals // ---------------------------------------------------------------------------- bool views_filters_modal(AppState& app) { if (!app.show_filters_modal) return false; bool changed = false; if (fn_ui::modal_dialog_begin("Filters", &app.show_filters_modal, ImVec2(520, 0))) { if (!app.graph) { ImGui::TextUnformatted("(no graph)"); } else { ImGui::TextUnformatted("Entity types"); ImGui::Separator(); int nt = app.type_visible_n; ImGui::Columns(2, "##fent", false); for (int i = 0; i < nt; ++i) { color_swatch(app.graph->types[i].color); char id[32]; std::snprintf(id, sizeof(id), "##fe%d", i); bool v = app.type_visible[i]; if (ImGui::Checkbox(id, &v)) { app.type_visible[i] = v; changed = true; } ImGui::SameLine(); ImGui::TextUnformatted(app.graph->types[i].name ? app.graph->types[i].name : "?"); ImGui::NextColumn(); } ImGui::Columns(1); if (app.graph->rel_type_count > 0) { ImGui::Spacing(); ImGui::TextUnformatted("Relation types"); ImGui::Separator(); int nr = app.rel_type_visible_n; ImGui::Columns(2, "##frel", false); for (int i = 0; i < nr; ++i) { color_swatch(app.graph->rel_types[i].color); char id[32]; std::snprintf(id, sizeof(id), "##fr%d", i); bool v = app.rel_type_visible[i]; if (ImGui::Checkbox(id, &v)) { app.rel_type_visible[i] = v; changed = true; } ImGui::SameLine(); ImGui::TextUnformatted(app.graph->rel_types[i].name ? app.graph->rel_types[i].name : "?"); ImGui::NextColumn(); } ImGui::Columns(1); } ImGui::Spacing(); if (fn_ui::button("Show all", fn_ui::ButtonVariant::Subtle)) { for (int i = 0; i < app.type_visible_n; ++i) app.type_visible[i] = true; for (int i = 0; i < app.rel_type_visible_n; ++i) app.rel_type_visible[i] = true; changed = true; } ImGui::SameLine(); if (fn_ui::button("Hide all", fn_ui::ButtonVariant::Subtle)) { for (int i = 0; i < app.type_visible_n; ++i) app.type_visible[i] = false; for (int i = 0; i < app.rel_type_visible_n; ++i) app.rel_type_visible[i] = false; changed = true; } ImGui::SameLine(); if (fn_ui::button("Close", fn_ui::ButtonVariant::Primary)) { app.show_filters_modal = false; } } } fn_ui::modal_dialog_end(); if (changed) views_apply_visibility(app); return changed; } bool views_open_modal(AppState& app) { if (!app.show_open_modal) return false; bool opened = false; if (fn_ui::modal_dialog_begin("Open file", &app.show_open_modal, ImVec2(520, 0))) { ImGui::TextWrapped("Path to operations.db (or any supported source)."); ImGui::Spacing(); fn_ui::text_input("Path", app.open_buf, sizeof(app.open_buf), "apps/registry_dashboard/operations.db"); ImGui::Spacing(); if (fn_ui::button("Open", fn_ui::ButtonVariant::Primary)) { if (app.open_buf[0]) { app.want_open_file = true; app.show_open_modal = false; opened = true; } } ImGui::SameLine(); if (fn_ui::button("Cancel", fn_ui::ButtonVariant::Subtle)) { app.show_open_modal = false; } } fn_ui::modal_dialog_end(); return opened; } } // namespace ge