/** * @file SourcePanel.cpp * @brief Source browser sidebar implementation. */ #include "SourcePanel.h" #include "App.h" #include "Protocol.h" #include "imgui.h" #include "implot.h" #include "Icons.h" #include #include namespace StreamHubClient { void RenderSourcePanel(App& app) { auto& sources = app.sources(); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.f, 2.f)); if (ImGui::CollapsingHeader("Sources", ImGuiTreeNodeFlags_DefaultOpen)) { for (int srcIdx = 0; srcIdx < static_cast(sources.size()); srcIdx++) { Source& src = sources[srcIdx]; /* State color indicator */ ImVec4 stateColor = (src.state == "connected") ? ImVec4(0.1f, 0.9f, 0.3f, 1.f) : ImVec4(0.8f, 0.3f, 0.3f, 1.f); ImGui::PushID(srcIdx); ImGui::TextColored(stateColor, ICON_FA_CIRCLE); ImGui::SameLine(); std::string treeLabel = src.label.empty() ? src.id : src.label; bool nodeOpen = ImGui::TreeNodeEx(treeLabel.c_str(), ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanAvailWidth); /* Per-source context menu: remove */ if (ImGui::BeginPopupContextItem("##srccmenu")) { if (ImGui::MenuItem(ICON_FA_TRASH_CAN " Remove source")) { app.ws().sendText(BuildRemoveSource(src.id)); } ImGui::EndPopup(); } if (nodeOpen) { for (int sigIdx = 0; sigIdx < static_cast(src.signals.size()); sigIdx++) { Signal& sig = src.signals[sigIdx]; ImGui::PushID(sigIdx); /* Color swatch */ ImVec4 col = sig.color; if (ImGui::ColorButton("##col", col, ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoBorder, ImVec2(12.f, 12.f))) { ImGui::OpenPopup("##sigcolor"); } if (ImGui::BeginPopup("##sigcolor")) { ImGui::ColorPicker4("Signal Color", reinterpret_cast(&sig.color), ImGuiColorEditFlags_NoAlpha); ImGui::EndPopup(); } ImGui::SameLine(); /* Unit / type label */ char label[128]; if (sig.meta.numElements > 1) { snprintf(label, sizeof(label), "%s [%u]", sig.meta.name.c_str(), sig.meta.numElements); } else { snprintf(label, sizeof(label), "%s", sig.meta.name.c_str()); } if (!sig.meta.unit.empty()) { strncat(label, " (", sizeof(label) - strlen(label) - 1); strncat(label, sig.meta.unit.c_str(), sizeof(label) - strlen(label) - 1); strncat(label, ")", sizeof(label) - strlen(label) - 1); } ImGui::Selectable(label, false, 0, ImVec2(0, 0)); /* Drag-and-drop source: payload = (srcIdx, sigIdx) */ if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) { struct DragPayload { int srcIdx; int sigIdx; }; DragPayload payload{srcIdx, sigIdx}; ImGui::SetDragDropPayload("SIGNAL_REF", &payload, sizeof(payload)); ImGui::TextUnformatted(sig.meta.name.c_str()); ImGui::EndDragDropSource(); } ImGui::PopID(); } ImGui::TreePop(); } ImGui::PopID(); } } ImGui::PopStyleVar(); /* ── Add / Save sources (inline at the bottom of the sidebar) ──────── */ ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); if (ImGui::Button(ICON_FA_PLUS " Add Source", ImVec2(-1.f, 0.f))) { app.showAddSrc() = true; } if (ImGui::Button(ICON_FA_FLOPPY_DISK " Save Sources", ImVec2(-1.f, 0.f))) { app.ws().sendText(BuildSaveSources()); } } } /* namespace StreamHubClient */