LCOV - code coverage report
Current view: top level - src/gui/pedalboard - pedal_board_chain.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 59.7 % 615 367
Test Date: 2026-06-03 09:13:19 Functions: 100.0 % 2 2

            Line data    Source code
       1              : #include "gui/commands/command.h"
       2              : #include "gui/pedalboard/pedal_board.h"
       3              : #include "gui/pedalboard/pedal_widget.h"
       4              : #include "gui/state/gui_graph_state.h"
       5              : #include "gui/theme/theme.h"
       6              : #include "gui/views/gui_midi.h"
       7              : #include <cmath>
       8              : #include <imgui.h>
       9              : #include <unordered_map>
      10              : namespace Amplitron {
      11          150 : void PedalBoard::render_signal_chain() {
      12          150 :   auto &ui_state = GuiGraphState::get_instance();
      13              :   // canvas_hovered is updated after the InvisibleButton is drawn (see below)
      14          150 :   float dt = ImGui::GetIO().DeltaTime;
      15          150 :   float lerp_factor = 1.0f - std::exp(-30.0f * dt);
      16          150 :   if (lerp_factor < 0.0f)
      17            0 :     lerp_factor = 0.0f;
      18          100 :   if (lerp_factor > 1.0f)
      19            0 :     lerp_factor = 1.0f;
      20          150 :   float old_zoom = ui_state.zoom;
      21          150 :   if (std::abs(ui_state.target_zoom - ui_state.zoom) > 0.0001f) {
      22           39 :     ui_state.zoom += (ui_state.target_zoom - ui_state.zoom) * lerp_factor;
      23           13 :   } else {
      24          111 :     ui_state.zoom = ui_state.target_zoom;
      25              :   }
      26          150 :   if (old_zoom != ui_state.zoom) {
      27              :     // Use canvas-local mouse coords to avoid drift when canvas is not at screen
      28              :     // origin
      29           39 :     float actual_factor = ui_state.zoom / old_zoom;
      30           39 :     ImVec2 mouse_pos = ImGui::GetMousePos();
      31           39 :     ImVec2 canvas_pos_now = ui_state.last_canvas_pos;
      32           39 :     float local_x = mouse_pos.x - canvas_pos_now.x;
      33           39 :     float local_y = mouse_pos.y - canvas_pos_now.y;
      34           39 :     ui_state.scrolling.x =
      35           39 :         local_x - (local_x - ui_state.scrolling.x) * actual_factor;
      36           39 :     ui_state.scrolling.y =
      37           39 :         local_y - (local_y - ui_state.scrolling.y) * actual_factor;
      38           39 :     ui_state.target_scrolling = ui_state.scrolling;
      39           13 :   }
      40          200 :   if (std::abs(ui_state.target_scrolling.x - ui_state.scrolling.x) > 0.01f ||
      41          150 :       std::abs(ui_state.target_scrolling.y - ui_state.scrolling.y) > 0.01f) {
      42            0 :     ui_state.scrolling.x +=
      43            0 :         (ui_state.target_scrolling.x - ui_state.scrolling.x) * lerp_factor;
      44            0 :     ui_state.scrolling.y +=
      45            0 :         (ui_state.target_scrolling.y - ui_state.scrolling.y) * lerp_factor;
      46            0 :   } else {
      47          150 :     ui_state.scrolling = ui_state.target_scrolling;
      48              :   }
      49          150 :   auto &audio_graph = engine_.graph();
      50          150 :   ImDrawList *draw_list = ImGui::GetWindowDrawList();
      51          150 :   ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
      52          150 :   ImVec2 canvas_size = ImGui::GetContentRegionAvail();
      53           50 :   ImVec2 canvas_end =
      54          150 :       ImVec2(canvas_pos.x + canvas_size.x, canvas_pos.y + canvas_size.y);
      55          150 :   ui_state.last_canvas_pos = canvas_pos;
      56          150 :   ImGui::SetCursorScreenPos(canvas_pos);
      57          150 :   ImGuiButtonFlags btn_flags = ImGuiButtonFlags_MouseButtonRight | ImGuiButtonFlags_MouseButtonMiddle | ImGuiButtonFlags_MouseButtonLeft;
      58              : 
      59          150 :   ImGui::SetNextItemAllowOverlap();
      60          150 :   if (canvas_size.x <= 0.0f) canvas_size.x = 1.0f;
      61          150 :   if (canvas_size.y <= 0.0f) canvas_size.y = 1.0f;
      62          150 :   ImGui::InvisibleButton("canvas_panning_hotspot", canvas_size, btn_flags);
      63              :   // Update canvas_hovered here — after InvisibleButton — so it reflects the
      64              :   // actual canvas item
      65          150 :   ui_state.canvas_hovered = ImGui::IsItemHovered();
      66              : 
      67          150 :   if (ImGui::IsItemHovered() && !ImGui::IsAnyItemHovered()) {
      68            0 :     ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
      69            0 :   }
      70              : 
      71          176 :   if (ImGui::IsItemActive() && (ImGui::IsMouseDragging(ImGuiMouseButton_Right) || 
      72           35 :                                 ImGui::IsMouseDragging(ImGuiMouseButton_Middle) || 
      73           24 :                                 ImGui::IsMouseDragging(ImGuiMouseButton_Left))) {
      74           12 :     ui_state.scrolling.x += ImGui::GetIO().MouseDelta.x;
      75           12 :     ui_state.scrolling.y += ImGui::GetIO().MouseDelta.y;
      76           12 :     ui_state.target_scrolling = ui_state.scrolling;
      77            4 :   }
      78              :   // Zooming is now allowed in both fullscreen and normal modes
      79          150 :   if (ImGui::IsItemHovered()) {
      80           87 :     float scroll_x = ImGui::GetIO().MouseWheelH;
      81           87 :     float scroll_y = ImGui::GetIO().MouseWheel;
      82           87 :     if (scroll_x != 0.0f || scroll_y != 0.0f) {
      83            9 :       if (ImGui::GetIO().KeyCtrl) {
      84            6 :         float zoom_factor = std::pow(1.1f, scroll_y);
      85            6 :         ImVec2 mouse_pos = ImGui::GetMousePos();
      86            2 :         ImVec2 mouse_in_canvas =
      87            6 :             ImVec2(mouse_pos.x - canvas_pos.x, mouse_pos.y - canvas_pos.y);
      88            6 :         float old_zoom = ui_state.target_zoom;
      89            6 :         float new_zoom = old_zoom * zoom_factor;
      90            6 :         if (new_zoom < 0.2f)
      91            0 :           new_zoom = 0.2f;
      92            6 :         if (new_zoom > 5.0f)
      93            0 :           new_zoom = 5.0f;
      94            6 :         float actual_factor = new_zoom / old_zoom;
      95            6 :         ui_state.target_scrolling.x =
      96            8 :             mouse_in_canvas.x -
      97            6 :             (mouse_in_canvas.x - ui_state.target_scrolling.x) * actual_factor;
      98            6 :         ui_state.target_scrolling.y =
      99            8 :             mouse_in_canvas.y -
     100            6 :             (mouse_in_canvas.y - ui_state.target_scrolling.y) * actual_factor;
     101            6 :         ui_state.target_zoom = new_zoom;
     102            2 :       } else {
     103            3 :         ui_state.target_scrolling.x += scroll_x * 20.0f;
     104            3 :         ui_state.target_scrolling.y += scroll_y * 20.0f;
     105              :       }
     106            3 :     }
     107           29 :   }
     108              :   // Draw fullscreen button at top right
     109          150 :   ImGui::SetCursorScreenPos(
     110          150 :       ImVec2(canvas_pos.x + canvas_size.x - 70, canvas_pos.y + 10));
     111          150 :   ImGui::SetNextItemAllowOverlap();
     112          200 :   if (ImGui::Button(ui_state.is_fullscreen ? "Exit FS" : "Full Screen")) {
     113            0 :     ui_state.is_fullscreen = !ui_state.is_fullscreen;
     114            0 :     if (!ui_state.is_fullscreen) {
     115            0 :       ui_state.zoom = 1.0f;
     116            0 :       ui_state.target_zoom = 1.0f;
     117            0 :     }
     118            0 :   }
     119          150 :   std::vector<int> stale_ids;
     120          503 :   for (auto it = ui_state.node_positions.begin(); it != ui_state.node_positions.end(); ) {
     121          353 :     bool found = false;
     122          641 :     for (const auto &node : audio_graph.get_nodes()) {
     123          589 :       if (node.id == it->first) { found = true; break; }
     124              :     }
     125          353 :     if (!found) { stale_ids.push_back(it->first); it = ui_state.node_positions.erase(it); }
     126          301 :     else ++it;
     127              :   }
     128          150 :   draw_list->PushClipRect(canvas_pos, canvas_end, true);
     129          150 :   ImVec2 offset(canvas_pos.x + ui_state.scrolling.x, canvas_pos.y + ui_state.scrolling.y);
     130          202 :   for (int id : stale_ids) {
     131           52 :     ui_state.node_positions.erase(id);
     132              :   }
     133              :   // Give all new nodes a default position at the end of the chain without
     134              :   // shifting existing nodes
     135          453 :   for (const auto &node : audio_graph.get_nodes()) {
     136          303 :     if (ui_state.node_positions.find(node.id) ==
     137          404 :         ui_state.node_positions.end()) {
     138            2 :       if (node.x != 0.0f || node.y != 0.0f) {
     139            2 :         ui_state.node_positions[node.id] = {ImVec2(node.x, node.y), false, ImVec2(0, 0)};
     140            0 :       } else {
     141            0 :         float max_right = 40.0f;
     142            0 :         for (const auto &existing_node : audio_graph.get_nodes()) {
     143            0 :           auto pos_it = ui_state.node_positions.find(existing_node.id);
     144            0 :           if (pos_it != ui_state.node_positions.end()) {
     145            0 :             float width =
     146            0 :                 (existing_node.routing_type == NodeRoutingType::StandardEffect)
     147            0 :                     ? 190.0f
     148              :                     : 110.0f;
     149            0 :             float right_edge = pos_it->second.position.x + width;
     150            0 :             if (right_edge > max_right) {
     151            0 :               max_right = right_edge;
     152            0 :             }
     153            0 :           }
     154              :         }
     155            0 :         float insert_x =
     156            0 :             ui_state.node_positions.empty() ? 40.0f : max_right + 80.0f;
     157            0 :         ui_state.node_positions[node.id] = {ImVec2(insert_x, 60.0f), false, ImVec2(0, 0)};
     158              :       }
     159            0 :     }
     160              :   }
     161              :   // Animation pulse based on time and audio level
     162          150 :   float level = engine_.get_output_level();
     163          150 :   float time = (float)ImGui::GetTime();
     164          200 :   bool is_running = engine_.is_running();
     165          150 :   int node_to_delete = -1;
     166          150 :   std::unordered_map<int, ImVec2> pin_positions_cache;
     167          453 :   for (const auto &node : audio_graph.get_nodes()) {
     168          303 :     auto &node_layout = ui_state.node_positions[node.id];
     169          101 :     ImVec2 node_screen_pos =
     170          404 :         ImVec2(offset.x + node_layout.position.x * ui_state.zoom,
     171          303 :                offset.y + node_layout.position.y * ui_state.zoom);
     172          303 :     PedalWidget *target_widget = nullptr;
     173          303 :     if (node.routing_type == NodeRoutingType::StandardEffect) {
     174          459 :       for (auto &w : widgets_) {
     175          309 :         if (w->get_effect() == node.pedal) {
     176          153 :           target_widget = w.get();
     177          153 :           break;
     178              :         }
     179              :       }
     180          101 :     }
     181          404 :     bool is_mb_comp = false;
     182          405 :     if (target_widget && std::strcmp(target_widget->get_effect()->name(),
     183           51 :                                      "MultiBand Compressor") == 0) {
     184            0 :       is_mb_comp = true;
     185            0 :     }
     186          404 :     float node_width =
     187          303 :         (target_widget ? (is_mb_comp ? 190.0f * 2.2f : 190.0f) : 110.0f) *
     188          303 :         ui_state.zoom;
     189          303 :     float node_height = (target_widget ? 360.0f : 70.0f) * ui_state.zoom;
     190          303 :     if (!target_widget && (node.routing_type == NodeRoutingType::Mixer ||
     191          100 :                            node.routing_type == NodeRoutingType::MergeSum)) {
     192            0 :       node_width = 130.0f * ui_state.zoom;
     193            0 :       node_height =
     194            0 :           std::max(70.0f, (float)node.input_pin_ids.size() * 32.0f + 55.0f) *
     195            0 :           ui_state.zoom;
     196          252 :     } else if (!target_widget && node.routing_type == NodeRoutingType::Splitter) {
     197            0 :       node_width = 130.0f * ui_state.zoom;
     198            0 :       node_height =
     199            0 :           std::max(70.0f, (float)node.output_pin_ids.size() * 32.0f + 55.0f) *
     200            0 :           ui_state.zoom;
     201            0 :     }
     202          303 :     ImGui::PushID(node.id);
     203          303 :     if (target_widget) {
     204          153 :       ImGui::SetCursorScreenPos(node_screen_pos);
     205          153 :       ImGui::BeginGroup();
     206          153 :       ImGui::SetWindowFontScale(ui_state.zoom);
     207          153 :       target_widget->render(ui_state.zoom);
     208          153 :       ImGui::SetWindowFontScale(1.0f);
     209          153 :       ImGui::EndGroup();
     210          153 :       ImGui::SetCursorScreenPos(node_screen_pos);
     211          153 :       ImGui::SetNextItemAllowOverlap();
     212          153 :       ImGui::InvisibleButton(
     213              :           "native_drag_handle",
     214          153 :           ImVec2(node_width - 25.0f * ui_state.zoom, 30.0f * ui_state.zoom));
     215          153 :       if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
     216            0 :         if (!node_layout.is_dragging) {
     217            0 :           node_layout.is_dragging = true;
     218            0 :           node_layout.drag_start_pos = node_layout.position;
     219            0 :         }
     220            0 :         node_layout.position.x += ImGui::GetIO().MouseDelta.x / ui_state.zoom;
     221            0 :         node_layout.position.y += ImGui::GetIO().MouseDelta.y / ui_state.zoom;
     222          153 :       } else if (node_layout.is_dragging && ImGui::IsItemDeactivated()) {
     223            0 :         node_layout.is_dragging = false;
     224            0 :         if (node_layout.position.x != node_layout.drag_start_pos.x ||
     225            0 :             node_layout.position.y != node_layout.drag_start_pos.y) {
     226            0 :           history_.push_executed(std::make_unique<MoveGraphNodeCommand>(
     227            0 :               node.id, node_layout.drag_start_pos, node_layout.position));
     228            0 :         }
     229            0 :       }
     230           51 :     } else {
     231          200 :       ImVec2 node_end = ImVec2(node_screen_pos.x + node_width,
     232          150 :                                node_screen_pos.y + node_height);
     233          150 :       ImU32 bg_color = IM_COL32(50, 35, 60, 255);
     234          200 :       draw_list->AddRectFilled(node_screen_pos, node_end, bg_color,
     235          150 :                                Theme::ROUNDING_MD * ui_state.zoom);
     236          200 :       draw_list->AddRect(node_screen_pos, node_end, IM_COL32(180, 140, 80, 180),
     237          100 :                          Theme::ROUNDING_MD * ui_state.zoom, 0,
     238          150 :                          1.5f * ui_state.zoom);
     239          150 :       ImGui::SetCursorScreenPos(node_screen_pos);
     240          150 :       ImGui::SetNextItemAllowOverlap();
     241          150 :       ImGui::InvisibleButton(
     242              :           "util_drag_handle",
     243          150 :           ImVec2(node_width - 25.0f * ui_state.zoom, node_height));
     244          150 :       if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
     245            0 :         if (!node_layout.is_dragging) {
     246            0 :           node_layout.is_dragging = true;
     247            0 :           node_layout.drag_start_pos = node_layout.position;
     248            0 :         }
     249            0 :         float node_width = (target_widget ? (is_mb_comp ? 190.0f * 2.2f : 190.0f) : 110.0f) * ui_state.zoom;
     250            0 :         float node_height = (target_widget ? 360.0f : 70.0f) * ui_state.zoom;
     251            0 :         ImGui::PushID(node.id);
     252            0 :         if (target_widget) {
     253            0 :             ImGui::SetCursorScreenPos(node_screen_pos);
     254            0 :             ImGui::BeginGroup();
     255            0 :             ImGui::SetWindowFontScale(ui_state.zoom);
     256            0 :             target_widget->render(ui_state.zoom); 
     257            0 :             ImGui::SetWindowFontScale(1.0f);
     258            0 :             ImGui::EndGroup();
     259            0 :             ImGui::SetCursorScreenPos(node_screen_pos);
     260            0 :             ImGui::SetNextItemAllowOverlap(); 
     261            0 :             ImGui::InvisibleButton("native_drag_handle", ImVec2(node_width - 25.0f * ui_state.zoom, 30.0f * ui_state.zoom));
     262            0 :             if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
     263            0 :                 if (!node_layout.is_dragging) {
     264            0 :                     node_layout.is_dragging = true;
     265            0 :                     node_layout.drag_start_pos = node_layout.position;
     266            0 :                 }
     267            0 :                 node_layout.position.x += ImGui::GetIO().MouseDelta.x / ui_state.zoom;
     268            0 :                 node_layout.position.y += ImGui::GetIO().MouseDelta.y / ui_state.zoom;
     269            0 :             } else if (node_layout.is_dragging && ImGui::IsItemDeactivated()) {
     270            0 :                 node_layout.is_dragging = false;
     271            0 :                 if (node_layout.position.x != node_layout.drag_start_pos.x || node_layout.position.y != node_layout.drag_start_pos.y) {
     272            0 :                     history_.push_executed(std::make_unique<MoveGraphNodeCommand>(node.id, node_layout.drag_start_pos, node_layout.position));
     273            0 :                 }
     274            0 :             }
     275            0 :         } else {
     276            0 :             ImVec2 node_end = ImVec2(node_screen_pos.x + node_width, node_screen_pos.y + node_height);
     277            0 :             ImU32 bg_color = IM_COL32(50, 35, 60, 255);
     278            0 :             draw_list->AddRectFilled(node_screen_pos, node_end, bg_color, Theme::ROUNDING_MD * ui_state.zoom);
     279            0 :             draw_list->AddRect(node_screen_pos, node_end, IM_COL32(180, 140, 80, 180), Theme::ROUNDING_MD * ui_state.zoom, 0, 1.5f * ui_state.zoom);
     280            0 :             ImGui::SetCursorScreenPos(node_screen_pos);
     281            0 :             ImGui::SetNextItemAllowOverlap();
     282            0 :             ImGui::InvisibleButton("util_drag_handle", ImVec2(node_width - 25.0f * ui_state.zoom, node_height));
     283            0 :             if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
     284            0 :                 if (!node_layout.is_dragging) {
     285            0 :                     node_layout.is_dragging = true;
     286            0 :                     node_layout.drag_start_pos = node_layout.position;
     287            0 :                 }
     288            0 :                 node_layout.position.x += ImGui::GetIO().MouseDelta.x / ui_state.zoom;
     289            0 :                 node_layout.position.y += ImGui::GetIO().MouseDelta.y / ui_state.zoom;
     290            0 :             } else if (node_layout.is_dragging && ImGui::IsItemDeactivated()) {
     291            0 :                 node_layout.is_dragging = false;
     292            0 :                 if (node_layout.position.x != node_layout.drag_start_pos.x || node_layout.position.y != node_layout.drag_start_pos.y) {
     293            0 :                     history_.push_executed(std::make_unique<MoveGraphNodeCommand>(node.id, node_layout.drag_start_pos, node_layout.position));
     294            0 :                 }
     295            0 :             }
     296            0 :             ImVec2 text_pos = ImVec2(node_screen_pos.x + 12.0f * ui_state.zoom, node_screen_pos.y + 25.0f * ui_state.zoom);
     297            0 :             ImGui::SetWindowFontScale(ui_state.zoom);
     298            0 :             draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 255), node.name.c_str());
     299            0 :             ImGui::SetWindowFontScale(1.0f);
     300              :         }
     301            0 :       }
     302          150 :       ImVec2 text_pos = ImVec2(node_screen_pos.x + 12.0f * ui_state.zoom, node_screen_pos.y + 25.0f * ui_state.zoom);
     303          150 :       ImGui::SetWindowFontScale(ui_state.zoom);
     304          150 :       draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 255), node.name.c_str());
     305          150 :       ImGui::SetWindowFontScale(1.0f);
     306              :     }
     307              : 
     308          404 :       bool is_mixer = (node.routing_type == NodeRoutingType::Mixer ||
     309          202 :                        node.routing_type == NodeRoutingType::MergeSum);
     310              : 
     311          303 :       if (is_mixer && ImGui::IsItemHovered() &&
     312            0 :           ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
     313            0 :         ImGui::OpenPopup("MixerContextMenu");
     314            0 :       }
     315          303 :       if (ImGui::BeginPopup("MixerContextMenu")) {
     316            0 :         if (ImGui::MenuItem("Add Input", nullptr, false,
     317            0 :                             node.input_pin_ids.size() < 8)) {
     318            0 :           audio_graph.add_input_pin(node.id);
     319            0 :           engine_.commit_graph_changes();
     320            0 :         }
     321            0 :         if (ImGui::MenuItem("Remove Last Input", nullptr, false,
     322            0 :                             node.input_pin_ids.size() > 2)) {
     323            0 :           if (audio_graph.remove_input_pin(node.id)) {
     324            0 :             engine_.commit_graph_changes();
     325            0 :           }
     326            0 :         }
     327            0 :         ImGui::EndPopup();
     328            0 :       }
     329              : 
     330          303 :       std::string display_name = node.name;
     331          303 :       if (is_mixer) {
     332            0 :         display_name = std::to_string(node.input_pin_ids.size()) + "-in Mixer";
     333            0 :       }
     334          404 :       ImVec2 text_pos = ImVec2(node_screen_pos.x + 12.0f * ui_state.zoom,
     335          303 :                                node_screen_pos.y + 10.0f * ui_state.zoom);
     336          303 :       ImGui::SetWindowFontScale(ui_state.zoom);
     337          404 :       draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 255),
     338          101 :                          display_name.c_str());
     339          303 :       ImGui::SetWindowFontScale(1.0f);
     340              : 
     341          303 :       if (is_mixer) {
     342            0 :         ImGui::SetWindowFontScale(ui_state.zoom * 0.8f);
     343            0 :         for (size_t idx = 0; idx < node.input_pin_ids.size(); ++idx) {
     344            0 :           float pin_y =
     345            0 :               node_screen_pos.y +
     346            0 :               (node_height * (idx + 1.0f) / (node.input_pin_ids.size() + 1.0f));
     347            0 :           ImGui::SetCursorScreenPos(
     348            0 :               ImVec2(node_screen_pos.x + 8.0f * ui_state.zoom,
     349            0 :                      pin_y - 8.0f * ui_state.zoom));
     350            0 :           ImGui::PushID(static_cast<int>(idx));
     351              :           
     352            0 :           ImGui::Text("In %d", (int)(idx + 1));
     353            0 :           ImGui::SameLine();
     354            0 :           ImGui::PushItemWidth(65.0f * ui_state.zoom);
     355              : 
     356            0 :           float gain = 1.0f;
     357            0 :           if (idx < node.input_gains.size())
     358            0 :             gain = node.input_gains[idx];
     359              : 
     360            0 :           if (ImGui::SliderFloat("##gain", &gain, 0.0f, 2.0f, "%.2f")) {
     361            0 :             audio_graph.set_mixer_input_gain(node.id, idx, gain);
     362            0 :             engine_.push_mixer_gain_change(node.id, idx, gain);
     363            0 :           }
     364              : 
     365            0 :           if (ImGui::IsItemHovered() &&
     366            0 :               ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
     367            0 :             ImGui::OpenPopup("GainMidiMenu");
     368            0 :           }
     369              : 
     370            0 :           if (ImGui::BeginPopup("GainMidiMenu")) {
     371            0 :             std::string effect_name = "Mixer_" + std::to_string(node.id);
     372            0 :             std::string param_name = "Gain " + std::to_string(idx);
     373            0 :             if (gui_midi_) {
     374            0 :               gui_midi_->render_remove_mapping_item(effect_name, param_name);
     375            0 :               gui_midi_->render_learn_menu_item(effect_name, param_name);
     376            0 :             } else {
     377            0 :               ImGui::TextDisabled("MIDI manager not available");
     378              :             }
     379            0 :             ImGui::EndPopup();
     380            0 :           }
     381              : 
     382            0 :           ImGui::PopItemWidth();
     383            0 :           ImGui::PopID();
     384            0 :         }
     385              :         
     386            0 :         ImGui::SetWindowFontScale(ui_state.zoom * 0.7f);
     387            0 :         ImGui::SetCursorScreenPos(
     388            0 :             ImVec2(node_screen_pos.x + 35.0f * ui_state.zoom,
     389            0 :                    node_screen_pos.y + node_height - 16.0f * ui_state.zoom));
     390            0 :         ImGui::TextDisabled("(Gain Fader)");
     391            0 :         ImGui::SetWindowFontScale(1.0f);
     392            0 :       }
     393          303 :     if (!node.is_reachable) {
     394            4 :       ImVec2 node_end = ImVec2(node_screen_pos.x + node_width,
     395            3 :                                node_screen_pos.y + node_height);
     396            4 :       draw_list->AddRectFilled(node_screen_pos, node_end,
     397              :                                IM_COL32(0, 0, 0, 180),
     398            3 :                                Theme::ROUNDING_MD * ui_state.zoom);
     399              : 
     400            1 :       ImVec2 text_pos =
     401            4 :           ImVec2(node_screen_pos.x + 10.0f * ui_state.zoom,
     402            3 :                  node_screen_pos.y + node_height - 25.0f * ui_state.zoom);
     403            3 :       ImGui::SetWindowFontScale(ui_state.zoom * 0.9f);
     404            3 :       draw_list->AddText(text_pos, IM_COL32(255, 60, 60, 255), "DISCONNECTED");
     405            3 :       ImGui::SetWindowFontScale(1.0f);
     406            1 :     }
     407              :     // --- INTERNAL SIGNAL FLOW / ELECTRICITY EFFECT ---
     408          403 :     if (node.is_reachable &&
     409          300 :         (!node.input_pin_ids.empty() || !node.output_pin_ids.empty())) {
     410          300 :       bool enabled = true;
     411          300 :       if (target_widget) {
     412          200 :         enabled = target_widget->get_effect()->is_enabled();
     413           50 :       }
     414              :       // Align with pins
     415          300 :       float in_y = node_screen_pos.y + node_height * 0.5f;
     416          300 :       if (!node.input_pin_ids.empty()) {
     417          600 :         in_y = node_screen_pos.y +
     418          300 :                (node_height * (0 + 1.0f) / (node.input_pin_ids.size() + 1.0f));
     419          100 :       }
     420          300 :       float out_y = node_screen_pos.y + node_height * 0.5f;
     421          300 :       if (!node.output_pin_ids.empty()) {
     422          600 :         out_y = node_screen_pos.y + (node_height * (0 + 1.0f) /
     423          300 :                                      (node.output_pin_ids.size() + 1.0f));
     424          100 :       }
     425              : 
     426          300 :       ImVec2 flow_p1(node_screen_pos.x, in_y);
     427          300 :       ImVec2 flow_p2(node_screen_pos.x + node_width, out_y);
     428              :       // --- COLOR & PULSE CALCULATIONS (SHARED) ---
     429          300 :       ImU32 flow_col = IM_COL32(200, 230, 255, 255);
     430          300 :       if (target_widget) {
     431          100 :         const auto *colors =
     432          150 :             get_effect_color(target_widget->get_effect()->name());
     433          150 :         flow_col = ImGui::ColorConvertFloat4ToU32(colors->led_color);
     434           50 :       }
     435              : 
     436          300 :       ImU32 r = (flow_col >> 0) & 0xFF;
     437          300 :       ImU32 g = (flow_col >> 8) & 0xFF;
     438          300 :       ImU32 b = (flow_col >> 16) & 0xFF;
     439          300 :       float pulse = 0.6f + 0.4f * std::sin(time * 8.0f) * (0.5f + level * 2.0f);
     440          300 :       float jitter = std::sin(time * 40.0f) * 1.5f * ui_state.zoom;
     441          300 :       if (enabled) {
     442              :         // --- ELECTRICITY PASSING THROUGH (ACTIVE) ---
     443              :         // No internal line, only glowing edges
     444          392 :         ImVec2 p_min(node_screen_pos.x - (2.0f + jitter),
     445          294 :                      node_screen_pos.y - (2.0f + jitter));
     446          392 :         ImVec2 p_max(node_screen_pos.x + node_width + (2.0f + jitter),
     447          294 :                      node_screen_pos.y + node_height + (2.0f + jitter));
     448              : 
     449          392 :         draw_list->AddRect(p_min, p_max, IM_COL32(r, g, b, (int)(180 * pulse)),
     450          196 :                            Theme::ROUNDING_MD * ui_state.zoom, 0,
     451          196 :                            3.0f * ui_state.zoom);
     452          392 :         draw_list->AddRect(
     453          294 :             p_min, p_max, IM_COL32(255, 255, 255, (int)(220 * pulse)),
     454          294 :             Theme::ROUNDING_MD * ui_state.zoom, 0, 1.0f * ui_state.zoom);
     455           98 :       } else {
     456              :         // --- ELECTRIC HIGH-RAIL BYPASS PATH ---
     457              :         // Rectangular "bridge" with CONSISTENT glow style
     458            6 :         float rail_height = 65.0f * ui_state.zoom;
     459            6 :         float rail_y = node_screen_pos.y - rail_height;
     460              : 
     461            6 :         ImVec2 p_rail_in(flow_p1.x, rail_y);
     462            6 :         ImVec2 p_rail_out(flow_p2.x, rail_y);
     463           22 :         auto draw_consistent_glow_line = [&](ImVec2 p1, ImVec2 p2) {
     464           18 :           ImVec2 p1j(p1.x, p1.y + jitter);
     465           18 :           ImVec2 p2j(p2.x, p2.y + jitter);
     466           24 :           draw_list->AddLine(p1j, p2j, IM_COL32(r, g, b, (int)(180 * pulse)),
     467           18 :                              4.0f * ui_state.zoom);
     468           24 :           draw_list->AddLine(p1j, p2j,
     469           18 :                              IM_COL32(255, 255, 255, (int)(220 * pulse)),
     470           18 :                              1.5f * ui_state.zoom);
     471           20 :         };
     472              :         // Bridge segments
     473            6 :         draw_consistent_glow_line(flow_p1, p_rail_in);
     474            6 :         draw_consistent_glow_line(p_rail_in, p_rail_out);
     475            6 :         draw_consistent_glow_line(p_rail_out, flow_p2);
     476              :         // --- MOVING ELECTRONS (PULSES) ---
     477              :         // Electrons travel along the bridge path: Up -> Across -> Down
     478            6 :         float electron_time = std::fmod(time * 2.5f, 1.0f);
     479           18 :         for (int e = 0; e < 2; ++e) {
     480           12 :           float t = std::fmod(electron_time + e * 0.5f, 1.0f);
     481           12 :           ImVec2 electron_pos;
     482              : 
     483           12 :           if (t < 0.2f) { // Segment 1: Up
     484            3 :             float st = t / 0.2f;
     485            3 :             electron_pos = ImVec2(
     486            3 :                 flow_p1.x, flow_p1.y + (rail_y - flow_p1.y) * st + jitter);
     487           10 :           } else if (t < 0.8f) { // Segment 2: Across
     488            6 :             float st = (t - 0.2f) / 0.6f;
     489            6 :             electron_pos =
     490            8 :                 ImVec2(p_rail_in.x + (p_rail_out.x - p_rail_in.x) * st,
     491            4 :                        rail_y + jitter);
     492            2 :           } else { // Segment 3: Down
     493            3 :             float st = (t - 0.8f) / 0.2f;
     494            4 :             electron_pos = ImVec2(p_rail_out.x,
     495            3 :                                   rail_y + (flow_p2.y - rail_y) * st + jitter);
     496              :           }
     497              : 
     498           16 :           draw_list->AddCircleFilled(
     499           12 :               electron_pos, 3.5f * ui_state.zoom,
     500           12 :               IM_COL32(255, 255, 255, (int)(220 * pulse)));
     501           16 :           draw_list->AddCircle(electron_pos, 7.0f * ui_state.zoom,
     502           12 :                                IM_COL32(r, g, b, (int)(180 * pulse)), 0,
     503           12 :                                2.0f * ui_state.zoom);
     504            4 :         }
     505            6 :         if (ImGui::IsMouseHoveringRect(ImVec2(node_screen_pos.x, rail_y - 10),
     506              :                                        flow_p2)) {
     507            0 :           ImGui::SetTooltip("%s (Bypassed - High Rail Signal Path)",
     508            0 :                             target_widget->get_effect()->name());
     509            0 :         }
     510              :       }
     511          300 :       if (ImGui::IsMouseHoveringRect(flow_p1, flow_p2) &&
     512            0 :           (target_widget || !node.name.empty())) {
     513            0 :         const char *name = target_widget ? target_widget->get_effect()->name()
     514            0 :                                          : node.name.c_str();
     515            0 :         ImGui::SetTooltip("%s (%s)", name, enabled ? "Active" : "Bypassed");
     516            0 :       }
     517          100 :     }
     518              :     // ====================================================================
     519              :     // THE DELETION [X] BUTTON
     520              :     // ====================================================================
     521          303 :     bool is_amp = (node.name == "Amp Sim");
     522          303 :     bool is_input_node = (node.name == "Input");
     523          303 :     if (!is_amp && !is_input_node) {
     524           48 :       ImVec2 cross_pos =
     525          192 :           ImVec2(node_screen_pos.x + node_width - 24.0f * ui_state.zoom,
     526          144 :                  node_screen_pos.y + 4.0f * ui_state.zoom);
     527          144 :       ImGui::SetCursorScreenPos(cross_pos);
     528              : 
     529              :       // Your exact color styling
     530          144 :       ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
     531          144 :       ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
     532           96 :                             ImVec4(0.6f, 0.1f, 0.1f, 0.8f));
     533              : 
     534              :       // Use SmallButton and exact string formatting
     535          144 :       std::string remove_label = "X##rm" + std::to_string(node.id);
     536          144 :       ImGui::SetNextItemAllowOverlap();
     537          144 :       if (ImGui::SmallButton(remove_label.c_str())) {
     538            0 :         node_to_delete = node.id;
     539            0 :       }
     540          144 :       if (ImGui::IsItemHovered()) {
     541            0 :         ImGui::SetTooltip("Remove %s from chain", node.name.c_str());
     542            0 :       }
     543              : 
     544          144 :       ImGui::PopStyleColor(2);
     545          144 :     }
     546              :     // ====================================================================
     547              :     // FIX: THE WIRE DROP ZONE (Input Pins)
     548              :     // ====================================================================
     549          303 :     if (!is_input_node) {
     550          306 :       for (size_t idx = 0; idx < node.input_pin_ids.size(); ++idx) {
     551          153 :         int pin_id = node.input_pin_ids[idx];
     552          255 :         float pin_y = node_screen_pos.y + (node_height * (idx + 1.0f) /
     553          153 :                                            (node.input_pin_ids.size() + 1.0f));
     554          153 :         ImVec2 pin_pos(node_screen_pos.x - 2.0f * ui_state.zoom, pin_y);
     555          153 :         pin_positions_cache[pin_id] = pin_pos;
     556          153 :         draw_list->AddCircleFilled(pin_pos, 5.0f * ui_state.zoom,
     557              :                                    IM_COL32(46, 204, 113, 255));
     558          153 :         draw_list->AddCircle(pin_pos, 6.5f * ui_state.zoom,
     559              :                              IM_COL32(255, 255, 255, 200));
     560          255 :         ImGui::SetCursorScreenPos(ImVec2(pin_pos.x - 10.0f * ui_state.zoom,
     561          153 :                                          pin_pos.y - 10.0f * ui_state.zoom));
     562          153 :         ImGui::PushID(pin_id);
     563          153 :         ImGui::SetNextItemAllowOverlap();
     564          153 :         ImGui::InvisibleButton(
     565          153 :             "in_pin", ImVec2(20.0f * ui_state.zoom, 20.0f * ui_state.zoom));
     566              : 
     567              :         // Check if hovered while releasing a dragged wire
     568          153 :         ImVec2 mouse_pos = ImGui::GetMousePos();
     569          153 :         float dist_sq =
     570          153 :             pow(mouse_pos.x - pin_pos.x, 2) + pow(mouse_pos.y - pin_pos.y, 2);
     571          153 :         if (dist_sq < pow(15.0f * ui_state.zoom, 2) &&
     572            0 :             ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
     573            0 :           if (ui_state.active_src_pin_id != -1) {
     574            0 :             history_.execute(std::make_unique<AddGraphLinkCommand>(
     575            0 :                 engine_, ui_state.active_src_pin_id, pin_id));
     576            0 :             ui_state.active_src_pin_id = -1;
     577            0 :           }
     578            0 :         }
     579          153 :         ImGui::PopID();
     580           51 :       }
     581           51 :     }
     582              :     // ====================================================================
     583              :     // FIX: THE WIRE DRAG START (Output Pins)
     584              :     // ====================================================================
     585          303 :     if (!is_amp) {
     586          588 :       for (size_t idx = 0; idx < node.output_pin_ids.size(); ++idx) {
     587          294 :         int pin_id = node.output_pin_ids[idx];
     588          490 :         float pin_y = node_screen_pos.y + (node_height * (idx + 1.0f) /
     589          294 :                                            (node.output_pin_ids.size() + 1.0f));
     590          392 :         ImVec2 pin_pos(node_screen_pos.x + node_width + 2.0f * ui_state.zoom,
     591          294 :                        pin_y);
     592          294 :         pin_positions_cache[pin_id] = pin_pos;
     593              :         // Track active wire position to snap to the pin perfectly
     594          294 :         if (ui_state.active_src_pin_id == pin_id)
     595            0 :           ui_state.active_src_pin_pos = pin_pos;
     596          294 :         draw_list->AddCircleFilled(pin_pos, 5.0f * ui_state.zoom,
     597              :                                    IM_COL32(231, 76, 60, 255));
     598          294 :         draw_list->AddCircle(pin_pos, 6.5f * ui_state.zoom,
     599              :                              IM_COL32(255, 255, 255, 200));
     600          490 :         ImGui::SetCursorScreenPos(ImVec2(pin_pos.x - 10.0f * ui_state.zoom,
     601          294 :                                          pin_pos.y - 10.0f * ui_state.zoom));
     602          294 :         ImGui::PushID(pin_id);
     603          294 :         ImGui::SetNextItemAllowOverlap();
     604          294 :         ImGui::InvisibleButton(
     605          294 :             "out_pin", ImVec2(20.0f * ui_state.zoom, 20.0f * ui_state.zoom));
     606              : 
     607              :         // Start drafting wire instantly on Mouse DOWN or delete on right click
     608          294 :         if (ImGui::IsItemHovered()) {
     609            0 :           if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
     610            0 :             ui_state.active_src_pin_id = pin_id;
     611            0 :             ui_state.active_src_pin_pos = pin_pos;
     612            0 :           } else if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
     613            0 :             auto links = audio_graph.get_links(); // copy
     614            0 :             for (const auto &l : links) {
     615            0 :               if (l.source_pin_id == pin_id) {
     616            0 :                 history_.execute(
     617            0 :                     std::make_unique<RemoveGraphLinkCommand>(engine_, l));
     618            0 :               }
     619              :             }
     620            0 :           }
     621            0 :         }
     622          294 :         ImGui::PopID();
     623           98 :       }
     624           98 :     }
     625          303 :     ImGui::PopID();
     626          303 :   }
     627              :   // Process Deletions safely after iterating
     628          150 :   if (node_to_delete != -1) {
     629            0 :     auto *node_ptr = audio_graph.find_node(node_to_delete);
     630            0 :     if (node_ptr && node_ptr->routing_type == NodeRoutingType::StandardEffect) {
     631            0 :       auto &effects = engine_.effects();
     632            0 :       for (size_t i = 0; i < effects.size(); ++i) {
     633            0 :         if (effects[i] == node_ptr->pedal) {
     634            0 :           history_.execute(std::make_unique<RemoveEffectCommand>(
     635            0 :               engine_, static_cast<int>(i)));
     636            0 :           rebuild_widgets();
     637            0 :           break;
     638              :         }
     639            0 :       }
     640            0 :     } else if (node_ptr) {
     641            0 :       history_.execute(std::make_unique<RemoveGraphNodeCommand>(
     642            0 :           engine_, node_to_delete, node_ptr->routing_type,
     643            0 :           ui_state.node_positions[node_to_delete].position));
     644            0 :     }
     645            0 :     ui_state.node_positions.erase(node_to_delete);
     646            0 :     ui_state.active_src_pin_id =
     647              :         -1; // avoid stale pin state after topology change
     648            0 :   }
     649              :   // Draw Patch Cables
     650          150 :   int link_to_delete = -1;
     651          300 :   for (const auto &link : audio_graph.get_links()) {
     652          250 :     if (pin_positions_cache.count(link.source_pin_id) &&
     653          150 :         pin_positions_cache.count(link.dest_pin_id)) {
     654          150 :       ImVec2 p1 = pin_positions_cache[link.source_pin_id];
     655          150 :       ImVec2 p2 = pin_positions_cache[link.dest_pin_id];
     656              : 
     657          150 :       ImVec2 cp1 = ImVec2(p1.x + 45.0f * ui_state.zoom, p1.y);
     658          150 :       ImVec2 cp2 = ImVec2(p2.x - 45.0f * ui_state.zoom, p2.y);
     659              :       // Distance detection for hovering/clicking
     660          150 :       bool hovered = false;
     661          150 :       ImVec2 mouse_pos = ImGui::GetMousePos();
     662         1650 :       for (float t = 0.0f; t <= 1.0f; t += 0.1f) {
     663         1500 :         float u = 1.0f - t;
     664         2500 :         float px = (u * u * u) * p1.x + (3 * u * u * t) * cp1.x +
     665         2000 :                    (3 * u * t * t) * cp2.x + (t * t * t) * p2.x;
     666         2500 :         float py = (u * u * u) * p1.y + (3 * u * u * t) * cp1.y +
     667         2000 :                    (3 * u * t * t) * cp2.y + (t * t * t) * p2.y;
     668         1500 :         float dx = px - mouse_pos.x;
     669         1500 :         float dy = py - mouse_pos.y;
     670         1500 :         if (dx * dx + dy * dy < 100.0f * ui_state.zoom * ui_state.zoom) {
     671            0 :           hovered = true;
     672            0 :           break;
     673              :         }
     674          500 :       }
     675          150 :       ImU32 color =
     676          150 :           hovered ? IM_COL32(255, 100, 100, 255) : IM_COL32(212, 175, 55, 255);
     677          250 :       draw_list->AddBezierCubic(p1, cp1, cp2, p2, color,
     678           50 :                                 hovered ? 5.0f * ui_state.zoom
     679          150 :                                         : 3.0f * ui_state.zoom);
     680              :       // --- Signal Pulse Animation on Cables ---
     681          150 :       if (is_running) {
     682            0 :         float t_off = std::fmod(time * 2.0f, 1.0f); // Slightly faster
     683            0 :         for (int step = 0; step < 4; ++step) {      // More pulses
     684            0 :           float t = std::fmod(t_off + step * 0.25f, 1.0f);
     685            0 :           float u = 1.0f - t;
     686            0 :           float hx = (u * u * u) * p1.x + (3 * u * u * t) * cp1.x +
     687            0 :                      (3 * u * t * t) * cp2.x + (t * t * t) * p2.x;
     688            0 :           float hy = (u * u * u) * p1.y + (3 * u * u * t) * cp1.y +
     689            0 :                      (3 * u * t * t) * cp2.y + (t * t * t) * p2.y;
     690              : 
     691              :           // Larger, more vibrant pulse
     692            0 :           float pulse_size = (3.0f + 2.0f * level) * ui_state.zoom;
     693            0 :           draw_list->AddCircleFilled(ImVec2(hx, hy), pulse_size,
     694              :                                      Theme::ACCENT_GOLD_HOT);
     695            0 :           draw_list->AddCircle(ImVec2(hx, hy),
     696            0 :                                pulse_size + 1.0f * ui_state.zoom,
     697              :                                IM_COL32(255, 255, 255, 150));
     698            0 :         }
     699            0 :       }
     700          150 :       if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
     701            0 :         link_to_delete = link.id;
     702            0 :       }
     703           50 :     }
     704              :   }
     705              : 
     706          150 :   if (link_to_delete != -1) {
     707            0 :     for (const auto &l : audio_graph.get_links()) {
     708            0 :       if (l.id == link_to_delete) {
     709            0 :         history_.execute(std::make_unique<RemoveGraphLinkCommand>(engine_, l));
     710            0 :         break;
     711              :       }
     712              :     }
     713            0 :   }
     714              :   // Draw Wire Spline Drafting
     715          150 :   if (ui_state.active_src_pin_id != -1) {
     716           12 :     ImVec2 mouse_pos = ImGui::GetMousePos();
     717           12 :     ImVec2 p1 = ui_state.active_src_pin_pos;
     718           12 :     ImVec2 cp1 = ImVec2(p1.x + 45.0f * ui_state.zoom, p1.y);
     719           12 :     ImVec2 cp2 = ImVec2(mouse_pos.x - 45.0f * ui_state.zoom, mouse_pos.y);
     720           12 :     draw_list->AddBezierCubic(p1, cp1, cp2, mouse_pos,
     721              :                               IM_COL32(255, 255, 255, 160), 2.0f, 0);
     722           12 :     if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
     723            6 :       ui_state.active_src_pin_id =
     724              :           -1; // Snap cable back if dropped in empty space
     725            2 :     }
     726            4 :   }
     727              :   // Fix ImGui cursor bounds warnings after free panning
     728          150 :   ImGui::SetCursorPos(ImVec2(0, 0));
     729          150 :   ImGui::Dummy(canvas_size);
     730          150 :   draw_list->PopClipRect();
     731          150 : }
     732              : } // namespace Amplitron
        

Generated by: LCOV version 2.0-1