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>
2026-10-27 16:00:29 +01:00
# include <FL/Fl_Group.H>
2026-10-26 23:55:35 +01:00
# include <FL/fl_draw.H>
2026-10-27 16:00:29 +01:00
# include <FL/Fl_Menu_Item.H>
2026-10-26 23:55:35 +01:00
# 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 :
2026-10-27 16:00:29 +01:00
return " Gain / bus " ;
2026-10-27 01:37:21 +01:00
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 :
2026-10-27 16:00:29 +01:00
return " MIDI mix " ;
2026-10-27 01:37:21 +01:00
case XTC_NODE_AUDIO :
2026-10-27 16:00:29 +01:00
return " Audio mix " ;
2026-10-27 01:37:21 +01:00
case XTC_NODE_OUTPUT :
2026-10-27 16:00:29 +01:00
return " Output sum " ;
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 ;
2026-10-27 16:00:29 +01:00
routing_count_ = 0 ;
( void ) xtc_graph_analyze ( graph_ , routing_ . data ( ) , routing_ . size ( ) , & routing_count_ ) ;
2026-10-26 23:55:35 +01:00
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 ;
2026-10-27 16:00:29 +01:00
for ( size_t j = 0U ; j < count_ ; + + j )
if ( std : : abs ( positions_ [ j ] . left - position . left ) < node_width + 12 & &
std : : abs ( positions_ [ j ] . top - position . top ) < node_height + 12 )
occupied = true ;
2026-10-27 01:37:21 +01:00
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 ;
2026-10-27 16:00:29 +01:00
grow_canvas ( ) ;
2026-10-26 23:55:35 +01:00
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 ;
2026-10-27 16:00:29 +01:00
describe_selection ( ) ;
2026-10-27 01:37:21 +01:00
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 ;
2026-10-27 16:00:29 +01:00
describe_selection ( ) ;
2026-10-27 01:37:21 +01:00
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. " ) ;
2026-10-27 16:00:29 +01:00
else if ( result = = XTC_ERR_GRAPH_CYCLE )
std : : snprintf ( text , sizeof ( text ) , " Graph unchanged: %s. Feedback loops are unsupported. " , xtc_result_string ( result ) ) ;
2026-10-27 01:37:21 +01:00
else
2026-10-27 16:00:29 +01:00
std : : snprintf ( text , sizeof ( text ) , " Graph unchanged: %s. Connect an audio output (right) to an input (left); duplicate wires are rejected. " , 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 ) ;
2026-10-27 16:00:29 +01:00
feedback ( result , " Selection deleted. Unwired inputs use external audio; select a node to inspect routing. " ) ;
2026-10-26 23:55:35 +01:00
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-27 16:00:29 +01:00
bool selected = ( edge_selected_ & & selected_edge_ . source_node = = edge . source_node & &
selected_edge_ . destination_node = = edge . destination_node ) | |
selected_node_ = = edge . source_node | | selected_node_ = = edge . destination_node ;
xtc_routing_info route { } ;
bool audible_path = routing ( edge . destination_node , route ) & & route . reaches_output ;
fl_color ( selected ? FL_YELLOW : audible_path ? fl_rgb_color ( 100 , 200 , 190 ) : fl_rgb_color ( 110 , 115 , 125 ) ) ;
2026-10-26 23:55:35 +01:00
fl_line_style ( FL_SOLID , selected ? 3 : 2 ) ;
fl_line ( x ( ) + sx , y ( ) + sy , x ( ) + dx , y ( ) + dy ) ;
2026-10-27 16:00:29 +01:00
double length = std : : hypot ( dx - sx , dy - sy ) ;
if ( length > 20 ) {
double ux = ( dx - sx ) / length , uy = ( dy - sy ) / length ;
int ax = x ( ) + ( sx + dx ) / 2 , ay = y ( ) + ( sy + dy ) / 2 ;
fl_line ( ax , ay , ax - static_cast < int > ( ux * 9 - uy * 5 ) , ay - static_cast < int > ( uy * 9 + ux * 5 ) ) ;
fl_line ( ax , ay , ax - static_cast < int > ( ux * 9 + uy * 5 ) , ay - static_cast < int > ( uy * 9 - ux * 5 ) ) ;
}
2026-10-26 23:55:35 +01:00
}
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 16:00:29 +01:00
fl_color ( node . type = = XTC_NODE_OUTPUT ? fl_rgb_color ( 100 , 50 , 55 )
: node . type = = XTC_NODE_GAIN ? fl_rgb_color ( 75 , 55 , 95 )
: node . type = = XTC_NODE_OSCILLATOR | | node . type = = XTC_NODE_AUDIO | | node . type = = XTC_NODE_INSTRUMENT
? fl_rgb_color ( 35 , 80 , 75 ) : fl_rgb_color ( 49 , 65 , 85 ) ) ;
2026-10-27 01:37:21 +01:00
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 ) ;
2026-10-27 16:00:29 +01:00
fl_push_clip ( left + 6 , top + 2 , node_width - 12 , 40 ) ;
2026-10-26 23:55:35 +01:00
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 "
2026-10-27 16:00:29 +01:00
: " Summed engine output " ) ;
2026-10-26 23:55:35 +01:00
fl_draw ( text , left + 10 , top + 37 ) ;
2026-10-27 16:00:29 +01:00
fl_pop_clip ( ) ;
xtc_routing_info route { } ;
if ( routing ( node . id , route ) & & ! route . reaches_output ) {
fl_color ( FL_YELLOW ) ;
fl_draw ( " ! no output path " , left + 10 , top + 72 ) ;
}
2026-10-26 23:55:35 +01:00
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 ) ;
2026-10-27 16:00:29 +01:00
fl_draw ( port = = 0U ? ( route . external_input ? " ext in " : " sum in " ) : " audio out " , left + ( port = = 0U ? 10 : 90 ) , top + 56 ) ;
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 16:00:29 +01:00
if ( Fl : : event_button ( ) = = FL_RIGHT_MOUSE ) {
Fl_Menu_Item items [ 5 ] { } ;
items [ 0 ] . text = " Inspect selection " ;
items [ 1 ] . text = " Arrange by signal flow (L) " ;
items [ 2 ] . text = " Insert unity gain in selected wire (I) " ;
items [ 3 ] . text = " Delete selection " ;
const auto * picked = items - > popup ( Fl : : event_x ( ) , Fl : : event_y ( ) ) ;
if ( picked = = items ) describe_selection ( ) ;
else if ( picked = = items + 1 ) auto_layout ( ) ;
else if ( picked = = items + 2 ) insert_gain ( ) ;
else if ( picked = = items + 3 ) delete_selected ( ) ;
return 1 ;
}
if ( Fl : : event_button ( ) ! = FL_LEFT_MOUSE ) return 0 ;
2026-10-27 01:37:21 +01:00
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 16:00:29 +01:00
if ( Fl : : event_key ( ) = = ' l ' & & ! Fl : : event_state ( FL_CTRL | FL_ALT ) ) { auto_layout ( ) ; return 1 ; }
if ( Fl : : event_key ( ) = = ' i ' & & ! Fl : : event_state ( FL_CTRL | FL_ALT ) ) { insert_gain ( ) ; return 1 ; }
if ( ( Fl : : event_key ( ) = = FL_Left | | Fl : : event_key ( ) = = FL_Right ) & & count_ ) {
size_t selected = 0 ;
for ( size_t i = 0 ; i < count_ ; + + i ) if ( positions_ [ i ] . id = = selected_node_ ) {
selected = Fl : : event_key ( ) = = FL_Left ? ( i + count_ - 1 ) % count_ : ( i + 1 ) % count_ ; break ;
}
select_node ( positions_ [ selected ] . id ) ;
return 1 ;
}
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 ) ;
}
2026-10-27 16:00:29 +01:00
bool GraphWidget : : routing ( size_t id , xtc_routing_info & info ) const
{
for ( size_t i = 0 ; i < routing_count_ ; + + i ) if ( routing_ [ i ] . id = = id ) { info = routing_ [ i ] ; return true ; }
return false ;
}
void GraphWidget : : grow_canvas ( )
{
int width = 2000 , height = 4000 ;
for ( size_t i = 0 ; i < count_ ; + + i ) {
width = std : : max ( width , positions_ [ i ] . left + node_width + 40 ) ;
height = std : : max ( height , positions_ [ i ] . top + node_height + 40 ) ;
}
size ( width , height ) ;
if ( parent ( ) ) parent ( ) - > redraw ( ) ;
}
bool GraphWidget : : auto_layout ( )
{
if ( routing_count_ ! = count_ ) return false ;
std : : array < size_t , 256 > rows { } ;
for ( size_t i = 0 ; i < count_ ; + + i ) {
xtc_routing_info info { } ;
if ( ! routing ( positions_ [ i ] . id , info ) | | info . level > = rows . size ( ) ) return false ;
positions_ [ i ] . left = 40 + static_cast < int > ( info . level ) * 210 ;
positions_ [ i ] . top = 40 + static_cast < int > ( rows [ info . level ] + + ) * 115 ;
}
wiring_ = dragging_ = false ;
grow_canvas ( ) ;
feedback_ - > copy_label ( " Arranged left to right by signal flow. Layout is session-only; no audio or routing changes. " ) ;
redraw ( ) ;
return true ;
}
xtc_result GraphWidget : : insert_gain ( )
{
if ( ! edge_selected_ ) {
feedback_ - > copy_label ( " Select a wire first, then insert a unity gain/bus node. " ) ;
return XTC_ERR_INVALID_STATE ;
}
xtc_graph_edit edit { } ;
edit . kind = XTC_EDIT_INSERT_GAIN ;
edit . data . connection = selected_edge_ ;
size_t added = SIZE_MAX ;
xtc_result result = handler_ ( context_ , edit , & added ) ;
if ( result = = XTC_OK ) select_node ( added ) ;
feedback ( result , " Unity gain inserted into wire as one edit. Inputs sum; fan-out copies the signal without normalization. " ) ;
return result ;
}
void GraphWidget : : describe_selection ( )
{
char text [ 320 ] ;
if ( edge_selected_ ) {
std : : snprintf ( text , sizeof ( text ) , " Wire: #%zu audio out -> #%zu sum in. Delete disconnects; I inserts a unity gain. Destination sums its inputs. " ,
selected_edge_ . source_node , selected_edge_ . destination_node ) ;
} else {
xtc_routing_info info { } ;
if ( ! routing ( selected_node_ , info ) ) { feedback_ - > copy_label ( " Select a node or wire to inspect routing. " ) ; return ; }
std : : snprintf ( text , sizeof ( text ) , " Node #%zu: %zu incoming, %zu outgoing | %s | %s. Ports are audio lanes; stereo uses paired independent processing. " ,
info . id , info . inputs , info . outputs ,
info . reaches_output ? " path to engine output " : " NO output path " ,
info . external_input ? " unwired input uses external audio " : " wired sum or source " ) ;
}
feedback_ - > copy_label ( text ) ;
}