xtc/src/gui/graph_widget.cpp

263 lines
11 KiB
C++
Raw Normal View History

#include "graph_widget.hpp"
#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;
const char *node_name(xtc_node_type type)
{
switch (type) {
case XTC_NODE_OSCILLATOR: return "Oscillator";
case XTC_NODE_GAIN: return "Gain";
case XTC_NODE_METER: return "Meter";
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";
}
return "Unknown";
}
}
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); }
void GraphWidget::set_graph(const xtc_graph *graph)
{
graph_ = graph;
size_t nodes = 0U, edges = 0U;
if (xtc_graph_get_counts(graph_, &nodes, &edges) != XTC_OK) { count_ = 0U; redraw(); return; }
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{};
if (xtc_graph_get_node(graph_, i, &node) != XTC_OK) continue;
Position position;
position.id = node.id;
bool found = false;
for (size_t j = 0U; j < old_count; ++j)
if (old[j].id == node.id) { position = old[j]; found = true; break; }
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 &&
std::abs(old[j].top - position.top) < node_height + 12) occupied = true;
if (!occupied) break;
}
}
positions_[count_++] = position;
}
int left, top;
if (!node_position(selected_node_, left, top)) selected_node_ = SIZE_MAX;
if (edge_selected_ && !select_connection(selected_edge_.source_node, selected_edge_.destination_node))
edge_selected_ = false;
redraw();
}
bool GraphWidget::node_position(size_t id, int &left, int &top) const
{
for (size_t i = 0U; i < count_; ++i) if (positions_[i].id == id) {
left = positions_[i].left; top = positions_[i].top; return true;
}
return false;
}
bool GraphWidget::move_node(size_t id, int left, int top)
{
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;
}
return false;
}
bool GraphWidget::select_node(size_t id)
{
int left, top;
if (!node_position(id, left, top)) return false;
selected_node_ = id; edge_selected_ = false; redraw(); return true;
}
bool GraphWidget::select_connection(size_t source, size_t destination)
{
size_t nodes, edges;
if (xtc_graph_get_counts(graph_, &nodes, &edges) != XTC_OK) return false;
for (size_t i = 0U; i < edges; ++i) {
xtc_connection_info edge{};
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;
}
}
return false;
}
void GraphWidget::feedback(xtc_result result, const char *success)
{
char text[256];
if (result == XTC_OK) std::snprintf(text, sizeof(text), "%s", success);
else if (result == XTC_ERR_CAPACITY)
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));
feedback_->copy_label(text);
}
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);
if (result == XTC_OK) select_node(added);
feedback(result, "Node added. Drag its title to move; drag an output port to an input to connect.");
return result;
}
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;
}
xtc_result GraphWidget::delete_selected()
{
xtc_graph_edit edit{};
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; }
xtc_result result = handler_(context_, edit, nullptr);
feedback(result, "Selection deleted.");
return result;
}
bool GraphWidget::port_position(size_t id, size_t port, int &left, int &top) const
{
xtc_port_info info{};
if (xtc_graph_get_port(graph_, id, port, &info) != XTC_OK || !node_position(id, left, top)) return false;
left += info.direction == XTC_PORT_OUTPUT ? node_width : 0;
top += 48;
return true;
}
bool GraphWidget::hit_port(int left, int top, size_t &node, size_t &port) const
{
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;
}
}
return false;
}
void GraphWidget::draw()
{
fl_push_clip(x(), y(), w(), h());
fl_color(fl_rgb_color(25, 28, 33)); fl_rectf(x(), y(), w(), h());
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;
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;
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{};
if (xtc_graph_get_node(graph_, i, &node) != XTC_OK) continue;
int left = x() + positions_[i].left, top = y() + positions_[i].top;
fl_color(fl_rgb_color(49, 54, 62)); fl_rectf(left, top, node_width, node_height);
fl_color(selected_node_ == node.id ? FL_YELLOW : fl_rgb_color(115, 130, 145));
fl_rect(left, top, node_width, node_height);
fl_color(FL_WHITE); fl_font(FL_HELVETICA_BOLD, 12);
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);
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));
else 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");
fl_draw(text, left + 10, top + 37);
for (size_t port = 0U; port < 2U; ++port) {
int px, py;
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);
}
}
if (wiring_) {
int px, py;
if (port_position(wire_node_, wire_port_, px, py)) {
fl_color(FL_YELLOW); fl_line(x() + px, y() + py, x() + mouse_x_, y() + mouse_y_);
}
}
fl_pop_clip();
}
int GraphWidget::handle(int event)
{
const int left = Fl::event_x() - x(), top = Fl::event_y() - y();
switch (event) {
case FL_FOCUS: case FL_UNFOCUS: return 1;
case FL_PUSH: {
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; }
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) {
select_node(pos.id); dragging_ = true; drag_x_ = left - pos.left; drag_y_ = top - pos.top; return 1;
}
}
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);
if (!port_position(edge.source_node, 1U, sx, sy) || !port_position(edge.destination_node, 0U, dx, dy)) continue;
double vx = dx - sx, vy = dy - sy;
double length = vx * vx + vy * vy;
double t = length == 0.0 ? 0.0 : std::max(0.0, std::min(1.0, ((left - sx) * vx + (top - sy) * vy) / length));
if (std::hypot(left - sx - t * vx, top - sy - t * vy) <= 6.0) {
select_connection(edge.source_node, edge.destination_node); return 1;
}
}
selected_node_ = SIZE_MAX; edge_selected_ = false; redraw(); return 1;
}
case FL_DRAG:
mouse_x_ = left; mouse_y_ = top;
if (dragging_) move_node(selected_node_, left - drag_x_, top - drag_y_);
if (wiring_) redraw();
return 1;
case FL_RELEASE:
dragging_ = false;
if (wiring_) {
size_t node, port;
wiring_ = false;
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.");
redraw();
}
return 1;
case FL_KEYDOWN:
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: break;
}
return Fl_Widget::handle(event);
}