2026-10-26 23:55:35 +01:00
|
|
|
#include "graph_widget.hpp"
|
2026-10-27 04:00:27 +01:00
|
|
|
#include "xtc/xtc_plugin.h"
|
2026-10-26 23:55:35 +01:00
|
|
|
#include <FL/Fl.H>
|
|
|
|
|
#include <FL/Fl_Box.H>
|
|
|
|
|
#include <FL/fl_draw.H>
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cmath>
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
constexpr int node_width = 150, node_height = 74;
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Returns the display label for a node type.
|
|
|
|
|
*
|
|
|
|
|
* @param type Node type to describe.
|
|
|
|
|
* @return Borrowed static string; never NULL, falling back to "Unknown".
|
|
|
|
|
*/
|
2026-10-26 23:55:35 +01:00
|
|
|
const char *node_name(xtc_node_type type)
|
|
|
|
|
{
|
|
|
|
|
switch (type) {
|
2026-10-27 01:37:21 +01:00
|
|
|
case XTC_NODE_OSCILLATOR:
|
|
|
|
|
return "Oscillator";
|
|
|
|
|
case XTC_NODE_GAIN:
|
|
|
|
|
return "Gain";
|
|
|
|
|
case XTC_NODE_METER:
|
|
|
|
|
return "Meter";
|
2026-10-27 04:00:27 +01:00
|
|
|
case XTC_NODE_PLUGIN:
|
|
|
|
|
return "LADSPA plugin";
|
2026-10-27 01:37:21 +01:00
|
|
|
case XTC_NODE_NATIVE:
|
|
|
|
|
return "Native processor";
|
|
|
|
|
case XTC_NODE_INSTRUMENT:
|
|
|
|
|
return "Instrument";
|
|
|
|
|
case XTC_NODE_AUDIO:
|
|
|
|
|
return "Audio clips";
|
|
|
|
|
case XTC_NODE_OUTPUT:
|
|
|
|
|
return "Output";
|
2026-10-26 23:55:35 +01:00
|
|
|
}
|
|
|
|
|
return "Unknown";
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
} // namespace
|
|
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Constructs the widget and initializes its cached layout from the graph.
|
|
|
|
|
*
|
|
|
|
|
* The graph, handler, context, and feedback box are stored without taking
|
|
|
|
|
* ownership. Layout population is delegated to set_graph().
|
|
|
|
|
*
|
|
|
|
|
* @param left Left coordinate in FLTK screen space.
|
|
|
|
|
* @param top Top coordinate in FLTK screen space.
|
|
|
|
|
* @param width Widget width in pixels.
|
|
|
|
|
* @param height Widget height in pixels.
|
|
|
|
|
* @param graph Borrowed graph snapshot; must outlive the widget's use of it.
|
|
|
|
|
* @param handler Edit callback invoked for every graph mutation.
|
|
|
|
|
* @param context Opaque pointer passed back to @p handler; may be NULL.
|
|
|
|
|
* @param feedback_box Borrowed label used to report edit results.
|
|
|
|
|
*/
|
2026-10-27 01:37:21 +01:00
|
|
|
GraphWidget::GraphWidget(int left, int top, int width, int height, const xtc_graph *graph, EditHandler handler,
|
|
|
|
|
void *context, Fl_Box *feedback_box)
|
|
|
|
|
: Fl_Widget(left, top, width, height), graph_(graph), handler_(handler), context_(context), feedback_(feedback_box)
|
|
|
|
|
{
|
|
|
|
|
set_graph(graph);
|
2026-10-26 23:55:35 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Replaces the graph snapshot and rebuilds cached node positions.
|
|
|
|
|
*
|
|
|
|
|
* Existing positions are preserved by node ID. New nodes get grid slots that do
|
|
|
|
|
* not overlap previously known nodes. The selection is dropped when the selected
|
|
|
|
|
* node or wire is no longer present. The widget does not take ownership of the
|
|
|
|
|
* graph.
|
|
|
|
|
*
|
|
|
|
|
* @param graph Borrowed graph that must outlive the widget's use of it.
|
|
|
|
|
*/
|
2026-10-26 23:55:35 +01:00
|
|
|
void GraphWidget::set_graph(const xtc_graph *graph)
|
|
|
|
|
{
|
|
|
|
|
graph_ = graph;
|
|
|
|
|
size_t nodes = 0U, edges = 0U;
|
2026-10-27 01:37:21 +01:00
|
|
|
if (xtc_graph_get_counts(graph_, &nodes, &edges) != XTC_OK) {
|
|
|
|
|
count_ = 0U;
|
|
|
|
|
redraw();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-10-26 23:55:35 +01:00
|
|
|
auto old = positions_;
|
|
|
|
|
size_t old_count = count_;
|
|
|
|
|
count_ = 0U;
|
|
|
|
|
for (size_t i = 0U; i < nodes && i < positions_.size(); ++i) {
|
|
|
|
|
xtc_node_info node{};
|
2026-10-27 01:37:21 +01:00
|
|
|
if (xtc_graph_get_node(graph_, i, &node) != XTC_OK)
|
|
|
|
|
continue;
|
2026-10-26 23:55:35 +01:00
|
|
|
Position position;
|
|
|
|
|
position.id = node.id;
|
|
|
|
|
bool found = false;
|
|
|
|
|
for (size_t j = 0U; j < old_count; ++j)
|
2026-10-27 01:37:21 +01:00
|
|
|
if (old[j].id == node.id) {
|
|
|
|
|
position = old[j];
|
|
|
|
|
found = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-10-26 23:55:35 +01:00
|
|
|
if (!found) {
|
|
|
|
|
for (size_t slot = 0U; slot <= positions_.size(); ++slot) {
|
|
|
|
|
position.left = 40 + static_cast<int>(slot % 8U) * 210;
|
|
|
|
|
position.top = 40 + static_cast<int>(slot / 8U) * 115;
|
|
|
|
|
bool occupied = false;
|
|
|
|
|
for (size_t j = 0U; j < old_count; ++j)
|
|
|
|
|
if (std::abs(old[j].left - position.left) < node_width + 12 &&
|
2026-10-27 01:37:21 +01:00
|
|
|
std::abs(old[j].top - position.top) < node_height + 12)
|
|
|
|
|
occupied = true;
|
|
|
|
|
if (!occupied)
|
|
|
|
|
break;
|
2026-10-26 23:55:35 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
positions_[count_++] = position;
|
|
|
|
|
}
|
|
|
|
|
int left, top;
|
2026-10-27 01:37:21 +01:00
|
|
|
if (!node_position(selected_node_, left, top))
|
|
|
|
|
selected_node_ = SIZE_MAX;
|
2026-10-26 23:55:35 +01:00
|
|
|
if (edge_selected_ && !select_connection(selected_edge_.source_node, selected_edge_.destination_node))
|
|
|
|
|
edge_selected_ = false;
|
|
|
|
|
redraw();
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Looks up the cached position of a node.
|
|
|
|
|
*
|
|
|
|
|
* @param id Stable node identifier.
|
|
|
|
|
* @param left Receives the left position in widget-local coordinates.
|
|
|
|
|
* @param top Receives the top position in widget-local coordinates.
|
|
|
|
|
* @return True when the node has a cached position, false otherwise.
|
|
|
|
|
*/
|
2026-10-26 23:55:35 +01:00
|
|
|
bool GraphWidget::node_position(size_t id, int &left, int &top) const
|
|
|
|
|
{
|
2026-10-27 01:37:21 +01:00
|
|
|
for (size_t i = 0U; i < count_; ++i)
|
|
|
|
|
if (positions_[i].id == id) {
|
|
|
|
|
left = positions_[i].left;
|
|
|
|
|
top = positions_[i].top;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-10-26 23:55:35 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Moves a node within the widget, clamped to the visible bounds.
|
|
|
|
|
*
|
|
|
|
|
* The node keeps a 12-pixel margin from the widget edges.
|
|
|
|
|
*
|
|
|
|
|
* @param id Stable node identifier.
|
|
|
|
|
* @param left Requested left position in widget-local coordinates.
|
|
|
|
|
* @param top Requested top position in widget-local coordinates.
|
|
|
|
|
* @return True when the node has a cached position, false otherwise.
|
|
|
|
|
*/
|
2026-10-26 23:55:35 +01:00
|
|
|
bool GraphWidget::move_node(size_t id, int left, int top)
|
|
|
|
|
{
|
2026-10-27 01:37:21 +01:00
|
|
|
for (size_t i = 0U; i < count_; ++i)
|
|
|
|
|
if (positions_[i].id == id) {
|
|
|
|
|
positions_[i].left = std::max(12, std::min(left, w() - node_width - 12));
|
|
|
|
|
positions_[i].top = std::max(12, std::min(top, h() - node_height - 12));
|
|
|
|
|
redraw();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-10-26 23:55:35 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Selects a node by ID and clears any wire selection.
|
|
|
|
|
*
|
|
|
|
|
* @param id Stable node identifier.
|
|
|
|
|
* @return True when the node has a cached position, false otherwise.
|
|
|
|
|
*/
|
2026-10-26 23:55:35 +01:00
|
|
|
bool GraphWidget::select_node(size_t id)
|
|
|
|
|
{
|
|
|
|
|
int left, top;
|
2026-10-27 01:37:21 +01:00
|
|
|
if (!node_position(id, left, top))
|
|
|
|
|
return false;
|
|
|
|
|
selected_node_ = id;
|
|
|
|
|
edge_selected_ = false;
|
|
|
|
|
redraw();
|
|
|
|
|
return true;
|
2026-10-26 23:55:35 +01:00
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Selects a wire between two nodes and clears the node selection.
|
|
|
|
|
*
|
|
|
|
|
* @param source Source node identifier.
|
|
|
|
|
* @param destination Destination node identifier.
|
|
|
|
|
* @return True when a matching connection exists, false otherwise.
|
|
|
|
|
*/
|
2026-10-26 23:55:35 +01:00
|
|
|
bool GraphWidget::select_connection(size_t source, size_t destination)
|
|
|
|
|
{
|
|
|
|
|
size_t nodes, edges;
|
2026-10-27 01:37:21 +01:00
|
|
|
if (xtc_graph_get_counts(graph_, &nodes, &edges) != XTC_OK)
|
|
|
|
|
return false;
|
2026-10-26 23:55:35 +01:00
|
|
|
for (size_t i = 0U; i < edges; ++i) {
|
|
|
|
|
xtc_connection_info edge{};
|
2026-10-27 01:37:21 +01:00
|
|
|
if (xtc_graph_get_connection(graph_, i, &edge) == XTC_OK && edge.source_node == source &&
|
|
|
|
|
edge.destination_node == destination) {
|
|
|
|
|
selected_edge_ = edge;
|
|
|
|
|
edge_selected_ = true;
|
|
|
|
|
selected_node_ = SIZE_MAX;
|
|
|
|
|
redraw();
|
|
|
|
|
return true;
|
2026-10-26 23:55:35 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Shows the outcome of an edit in the feedback label.
|
|
|
|
|
*
|
|
|
|
|
* @param result Result returned by the edit handler.
|
|
|
|
|
* @param success Text shown when @p result is XTC_OK; must be valid then and is ignored otherwise.
|
|
|
|
|
*/
|
2026-10-26 23:55:35 +01:00
|
|
|
void GraphWidget::feedback(xtc_result result, const char *success)
|
|
|
|
|
{
|
|
|
|
|
char text[256];
|
2026-10-27 01:37:21 +01:00
|
|
|
if (result == XTC_OK)
|
|
|
|
|
std::snprintf(text, sizeof(text), "%s", success);
|
2026-10-26 23:55:35 +01:00
|
|
|
else if (result == XTC_ERR_CAPACITY)
|
2026-10-27 01:37:21 +01:00
|
|
|
std::snprintf(
|
|
|
|
|
text, sizeof(text),
|
|
|
|
|
"Graph unchanged: capacity exceeded (node limit or pending edit). Retry after the next audio block.");
|
|
|
|
|
else
|
|
|
|
|
std::snprintf(text, sizeof(text), "Graph unchanged: %s.", xtc_result_string(result));
|
2026-10-26 23:55:35 +01:00
|
|
|
feedback_->copy_label(text);
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Requests creation of a node through the edit handler.
|
|
|
|
|
*
|
|
|
|
|
* Selects the new node when the edit succeeds.
|
|
|
|
|
*
|
|
|
|
|
* @param type Kind of node to add.
|
|
|
|
|
* @return Result reported by the edit handler.
|
|
|
|
|
*/
|
2026-10-26 23:55:35 +01:00
|
|
|
xtc_result GraphWidget::add_node(xtc_node_type type)
|
|
|
|
|
{
|
|
|
|
|
xtc_graph_edit edit{};
|
|
|
|
|
edit.kind = XTC_EDIT_ADD_NODE;
|
|
|
|
|
edit.data.add.type = type;
|
|
|
|
|
edit.data.add.value = type == XTC_NODE_OSCILLATOR ? 440.0f : type == XTC_NODE_GAIN ? 0.1f : 0.0f;
|
|
|
|
|
size_t added = SIZE_MAX;
|
|
|
|
|
xtc_result result = handler_(context_, edit, &added);
|
2026-10-27 01:37:21 +01:00
|
|
|
if (result == XTC_OK)
|
|
|
|
|
select_node(added);
|
2026-10-26 23:55:35 +01:00
|
|
|
feedback(result, "Node added. Drag its title to move; drag an output port to an input to connect.");
|
|
|
|
|
return result;
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Requests a connection between two ports through the edit handler.
|
|
|
|
|
*
|
|
|
|
|
* @param connection Connection describing the source and destination node/port pairs.
|
|
|
|
|
* @return Result reported by the edit handler.
|
|
|
|
|
*/
|
2026-10-26 23:55:35 +01:00
|
|
|
xtc_result GraphWidget::connect_ports(const xtc_connection_info &connection)
|
|
|
|
|
{
|
|
|
|
|
xtc_graph_edit edit{};
|
|
|
|
|
edit.kind = XTC_EDIT_CONNECT;
|
|
|
|
|
edit.data.connection = connection;
|
|
|
|
|
xtc_result result = handler_(context_, edit, nullptr);
|
|
|
|
|
feedback(result, "Connection added. Click a wire and press Delete to disconnect.");
|
|
|
|
|
return result;
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Requests removal of the selected wire or node through the edit handler.
|
|
|
|
|
*
|
|
|
|
|
* @return Result reported by the edit handler, or XTC_ERR_INVALID_STATE when
|
|
|
|
|
* nothing is selected.
|
|
|
|
|
*/
|
2026-10-26 23:55:35 +01:00
|
|
|
xtc_result GraphWidget::delete_selected()
|
|
|
|
|
{
|
|
|
|
|
xtc_graph_edit edit{};
|
2026-10-27 01:37:21 +01:00
|
|
|
if (edge_selected_) {
|
|
|
|
|
edit.kind = XTC_EDIT_DISCONNECT;
|
|
|
|
|
edit.data.connection = selected_edge_;
|
|
|
|
|
} else if (selected_node_ != SIZE_MAX) {
|
|
|
|
|
edit.kind = XTC_EDIT_REMOVE_NODE;
|
|
|
|
|
edit.data.node_id = selected_node_;
|
|
|
|
|
} else {
|
|
|
|
|
feedback_->copy_label("Select a node or wire first.");
|
|
|
|
|
return XTC_ERR_INVALID_STATE;
|
|
|
|
|
}
|
2026-10-26 23:55:35 +01:00
|
|
|
xtc_result result = handler_(context_, edit, nullptr);
|
|
|
|
|
feedback(result, "Selection deleted.");
|
|
|
|
|
return result;
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Computes the widget-local center of a node port.
|
|
|
|
|
*
|
|
|
|
|
* @param id Stable node identifier.
|
|
|
|
|
* @param port Port index; 0 selects the input and 1 the output.
|
|
|
|
|
* @param left Receives the port center x coordinate.
|
|
|
|
|
* @param top Receives the port center y coordinate.
|
|
|
|
|
* @return True when the node and port exist, false otherwise.
|
|
|
|
|
*/
|
2026-10-26 23:55:35 +01:00
|
|
|
bool GraphWidget::port_position(size_t id, size_t port, int &left, int &top) const
|
|
|
|
|
{
|
|
|
|
|
xtc_port_info info{};
|
2026-10-27 01:37:21 +01:00
|
|
|
if (xtc_graph_get_port(graph_, id, port, &info) != XTC_OK || !node_position(id, left, top))
|
|
|
|
|
return false;
|
2026-10-26 23:55:35 +01:00
|
|
|
left += info.direction == XTC_PORT_OUTPUT ? node_width : 0;
|
|
|
|
|
top += 48;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Finds the port nearest to a widget-local point.
|
|
|
|
|
*
|
|
|
|
|
* Searches nodes in reverse draw order and accepts points within 9 pixels of a
|
|
|
|
|
* port center.
|
|
|
|
|
*
|
|
|
|
|
* @param left Widget-local x coordinate to test.
|
|
|
|
|
* @param top Widget-local y coordinate to test.
|
|
|
|
|
* @param node Receives the matching node ID when true is returned.
|
|
|
|
|
* @param port Receives the matching port index (0 input, 1 output) when true is returned.
|
|
|
|
|
* @return True when a port is within range, false otherwise.
|
|
|
|
|
*/
|
2026-10-26 23:55:35 +01:00
|
|
|
bool GraphWidget::hit_port(int left, int top, size_t &node, size_t &port) const
|
|
|
|
|
{
|
2026-10-27 01:37:21 +01:00
|
|
|
for (size_t i = count_; i > 0U; --i)
|
|
|
|
|
for (size_t p = 0U; p < 2U; ++p) {
|
|
|
|
|
int px, py;
|
|
|
|
|
if (port_position(positions_[i - 1U].id, p, px, py) && std::abs(px - left) <= 9 &&
|
|
|
|
|
std::abs(py - top) <= 9) {
|
|
|
|
|
node = positions_[i - 1U].id;
|
|
|
|
|
port = p;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-10-26 23:55:35 +01:00
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Draws the background, wires, nodes, ports, and any pending wire.
|
|
|
|
|
*/
|
2026-10-26 23:55:35 +01:00
|
|
|
void GraphWidget::draw()
|
|
|
|
|
{
|
|
|
|
|
fl_push_clip(x(), y(), w(), h());
|
2026-10-27 01:37:21 +01:00
|
|
|
fl_color(fl_rgb_color(25, 28, 33));
|
|
|
|
|
fl_rectf(x(), y(), w(), h());
|
2026-10-26 23:55:35 +01:00
|
|
|
size_t nodes = 0U, edges = 0U;
|
|
|
|
|
(void)xtc_graph_get_counts(graph_, &nodes, &edges);
|
|
|
|
|
for (size_t i = 0U; i < edges; ++i) {
|
|
|
|
|
xtc_connection_info edge{};
|
|
|
|
|
int sx, sy, dx, dy;
|
2026-10-27 01:37:21 +01:00
|
|
|
if (xtc_graph_get_connection(graph_, i, &edge) != XTC_OK || !port_position(edge.source_node, 1U, sx, sy) ||
|
|
|
|
|
!port_position(edge.destination_node, 0U, dx, dy))
|
|
|
|
|
continue;
|
2026-10-26 23:55:35 +01:00
|
|
|
bool selected = edge_selected_ && selected_edge_.source_node == edge.source_node &&
|
|
|
|
|
selected_edge_.destination_node == edge.destination_node;
|
|
|
|
|
fl_color(selected ? FL_YELLOW : fl_rgb_color(100, 200, 190));
|
|
|
|
|
fl_line_style(FL_SOLID, selected ? 3 : 2);
|
|
|
|
|
fl_line(x() + sx, y() + sy, x() + dx, y() + dy);
|
|
|
|
|
}
|
|
|
|
|
fl_line_style(0);
|
|
|
|
|
for (size_t i = 0U; i < count_; ++i) {
|
|
|
|
|
xtc_node_info node{};
|
2026-10-27 01:37:21 +01:00
|
|
|
if (xtc_graph_get_node(graph_, i, &node) != XTC_OK)
|
|
|
|
|
continue;
|
2026-10-26 23:55:35 +01:00
|
|
|
int left = x() + positions_[i].left, top = y() + positions_[i].top;
|
2026-10-27 01:37:21 +01:00
|
|
|
fl_color(fl_rgb_color(49, 54, 62));
|
|
|
|
|
fl_rectf(left, top, node_width, node_height);
|
2026-10-26 23:55:35 +01:00
|
|
|
fl_color(selected_node_ == node.id ? FL_YELLOW : fl_rgb_color(115, 130, 145));
|
|
|
|
|
fl_rect(left, top, node_width, node_height);
|
2026-10-27 01:37:21 +01:00
|
|
|
fl_color(FL_WHITE);
|
|
|
|
|
fl_font(FL_HELVETICA_BOLD, 12);
|
2026-10-26 23:55:35 +01:00
|
|
|
char text[96];
|
|
|
|
|
std::snprintf(text, sizeof(text), "%s #%zu", node_name(node.type), node.id);
|
|
|
|
|
fl_draw(text, left + 10, top + 20);
|
|
|
|
|
fl_font(FL_HELVETICA, 12);
|
2026-10-27 01:37:21 +01:00
|
|
|
if (node.type == XTC_NODE_OSCILLATOR)
|
|
|
|
|
std::snprintf(text, sizeof(text), "%.0f Hz", static_cast<double>(node.value));
|
|
|
|
|
else if (node.type == XTC_NODE_GAIN)
|
|
|
|
|
std::snprintf(text, sizeof(text), "x %.2f", static_cast<double>(node.value));
|
2026-10-27 04:00:27 +01:00
|
|
|
else if (node.type == XTC_NODE_PLUGIN) {
|
|
|
|
|
xtc_plugin_spec plugin{};
|
|
|
|
|
if (xtc_graph_get_plugin(graph_,node.id,&plugin) == XTC_OK)
|
|
|
|
|
std::snprintf(text,sizeof(text),"%.20s",plugin.label);
|
|
|
|
|
} else
|
2026-10-27 01:37:21 +01:00
|
|
|
std::snprintf(text, sizeof(text), "%s",
|
|
|
|
|
node.type == XTC_NODE_METER ? "Peak pass-through"
|
|
|
|
|
: node.type == XTC_NODE_INSTRUMENT ? "MIDI sine source"
|
|
|
|
|
: node.type == XTC_NODE_AUDIO ? "Timeline mix source"
|
|
|
|
|
: "Mono audio output");
|
2026-10-26 23:55:35 +01:00
|
|
|
fl_draw(text, left + 10, top + 37);
|
|
|
|
|
for (size_t port = 0U; port < 2U; ++port) {
|
|
|
|
|
int px, py;
|
2026-10-27 01:37:21 +01:00
|
|
|
if (!port_position(node.id, port, px, py))
|
|
|
|
|
continue;
|
|
|
|
|
fl_color(fl_rgb_color(110, 220, 185));
|
|
|
|
|
fl_pie(x() + px - 5, y() + py - 5, 10, 10, 0, 360);
|
|
|
|
|
fl_color(FL_WHITE);
|
|
|
|
|
fl_draw(port == 0U ? "in" : "out", left + (port == 0U ? 10 : 118), top + 62);
|
2026-10-26 23:55:35 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (wiring_) {
|
|
|
|
|
int px, py;
|
|
|
|
|
if (port_position(wire_node_, wire_port_, px, py)) {
|
2026-10-27 01:37:21 +01:00
|
|
|
fl_color(FL_YELLOW);
|
|
|
|
|
fl_line(x() + px, y() + py, x() + mouse_x_, y() + mouse_y_);
|
2026-10-26 23:55:35 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fl_pop_clip();
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Handles mouse and keyboard events for selection, dragging, and wiring.
|
|
|
|
|
*
|
|
|
|
|
* @param event FLTK event code.
|
|
|
|
|
* @return 1 when the event was consumed, 0 for unsupported left-button input,
|
|
|
|
|
* or the result of Fl_Widget::handle() for unhandled events.
|
|
|
|
|
*/
|
2026-10-26 23:55:35 +01:00
|
|
|
int GraphWidget::handle(int event)
|
|
|
|
|
{
|
|
|
|
|
const int left = Fl::event_x() - x(), top = Fl::event_y() - y();
|
|
|
|
|
switch (event) {
|
2026-10-27 01:37:21 +01:00
|
|
|
case FL_FOCUS:
|
|
|
|
|
case FL_UNFOCUS:
|
|
|
|
|
return 1;
|
2026-10-26 23:55:35 +01:00
|
|
|
case FL_PUSH: {
|
2026-10-27 01:37:21 +01:00
|
|
|
if (Fl::event_button() != FL_LEFT_MOUSE)
|
|
|
|
|
return 0;
|
|
|
|
|
take_focus();
|
|
|
|
|
mouse_x_ = left;
|
|
|
|
|
mouse_y_ = top;
|
|
|
|
|
dragging_ = false;
|
|
|
|
|
wiring_ = false;
|
|
|
|
|
if (hit_port(left, top, wire_node_, wire_port_)) {
|
|
|
|
|
wiring_ = true;
|
|
|
|
|
redraw();
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
2026-10-26 23:55:35 +01:00
|
|
|
for (size_t i = count_; i > 0U; --i) {
|
|
|
|
|
const Position &pos = positions_[i - 1U];
|
|
|
|
|
if (left >= pos.left && left <= pos.left + node_width && top >= pos.top && top <= pos.top + node_height) {
|
2026-10-27 01:37:21 +01:00
|
|
|
select_node(pos.id);
|
|
|
|
|
dragging_ = true;
|
|
|
|
|
drag_x_ = left - pos.left;
|
|
|
|
|
drag_y_ = top - pos.top;
|
|
|
|
|
return 1;
|
2026-10-26 23:55:35 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
size_t nodes, edges;
|
|
|
|
|
(void)xtc_graph_get_counts(graph_, &nodes, &edges);
|
|
|
|
|
for (size_t i = 0U; i < edges; ++i) {
|
|
|
|
|
xtc_connection_info edge{};
|
|
|
|
|
int sx, sy, dx, dy;
|
|
|
|
|
(void)xtc_graph_get_connection(graph_, i, &edge);
|
2026-10-27 01:37:21 +01:00
|
|
|
if (!port_position(edge.source_node, 1U, sx, sy) || !port_position(edge.destination_node, 0U, dx, dy))
|
|
|
|
|
continue;
|
2026-10-26 23:55:35 +01:00
|
|
|
double vx = dx - sx, vy = dy - sy;
|
|
|
|
|
double length = vx * vx + vy * vy;
|
2026-10-27 01:37:21 +01:00
|
|
|
double t =
|
|
|
|
|
length == 0.0 ? 0.0 : std::max(0.0, std::min(1.0, ((left - sx) * vx + (top - sy) * vy) / length));
|
2026-10-26 23:55:35 +01:00
|
|
|
if (std::hypot(left - sx - t * vx, top - sy - t * vy) <= 6.0) {
|
2026-10-27 01:37:21 +01:00
|
|
|
select_connection(edge.source_node, edge.destination_node);
|
|
|
|
|
return 1;
|
2026-10-26 23:55:35 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
selected_node_ = SIZE_MAX;
|
|
|
|
|
edge_selected_ = false;
|
|
|
|
|
redraw();
|
|
|
|
|
return 1;
|
2026-10-26 23:55:35 +01:00
|
|
|
}
|
|
|
|
|
case FL_DRAG:
|
2026-10-27 01:37:21 +01:00
|
|
|
mouse_x_ = left;
|
|
|
|
|
mouse_y_ = top;
|
|
|
|
|
if (dragging_)
|
|
|
|
|
move_node(selected_node_, left - drag_x_, top - drag_y_);
|
|
|
|
|
if (wiring_)
|
|
|
|
|
redraw();
|
2026-10-26 23:55:35 +01:00
|
|
|
return 1;
|
|
|
|
|
case FL_RELEASE:
|
|
|
|
|
dragging_ = false;
|
|
|
|
|
if (wiring_) {
|
|
|
|
|
size_t node, port;
|
|
|
|
|
wiring_ = false;
|
2026-10-27 01:37:21 +01:00
|
|
|
if (hit_port(left, top, node, port))
|
|
|
|
|
connect_ports({wire_node_, wire_port_, node, port});
|
|
|
|
|
else
|
|
|
|
|
feedback_->copy_label("Connection cancelled. Drag an output port to an input port.");
|
2026-10-26 23:55:35 +01:00
|
|
|
redraw();
|
|
|
|
|
}
|
|
|
|
|
return 1;
|
|
|
|
|
case FL_KEYDOWN:
|
2026-10-27 01:37:21 +01:00
|
|
|
if (Fl::event_key() == FL_Delete || Fl::event_key() == FL_BackSpace) {
|
|
|
|
|
delete_selected();
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
if (Fl::event_key() == FL_Escape) {
|
|
|
|
|
wiring_ = dragging_ = false;
|
|
|
|
|
redraw();
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
default:
|
2026-10-26 23:55:35 +01:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
return Fl_Widget::handle(event);
|
|
|
|
|
}
|