feat: a wire is dragged from port to port, and a wired input is dragged off
One state machine decided at `drag-begin` -- a port starts a wire, a box moves it, the background pans -- so the two-click protocol and its armed-port highlight are gone. A drag from an input empties it immediately, which makes a release over nothing the detach gesture; `Escape` puts it back.
This commit is contained in:
1 parent
9f589d5525
commit
0b5ace9585
1 file changed
+218
-37
+218
-37
@@ -3287,14 +3287,25 @@ constexpr double kNodeW = 132.0;
|
||||
constexpr double kNodeH = 46.0;
|
||||
constexpr double kPort = 10.0;
|
||||
|
||||
enum class MathDrag { None, Node, Wire, Pan };
|
||||
|
||||
struct MathDialog {
|
||||
App* app = nullptr;
|
||||
int edit = -1;
|
||||
std::vector<MathNode> nodes;
|
||||
int output = 0;
|
||||
int selected = 0;
|
||||
/// The node whose output square was clicked and is waiting for an input to connect to.
|
||||
int pending_out = -1;
|
||||
/// What a drag is doing, decided once at `drag-begin` from what is under the press and not
|
||||
/// revisited: a gesture that changes its mind mid-drag is unusable. `Wire` carries a loose end
|
||||
/// in graph units, which the draw shows so the user can see what is being connected.
|
||||
MathDrag drag_mode = MathDrag::None;
|
||||
int wire_from = -1;
|
||||
int wire_detach = -1;
|
||||
int wire_detach_which = 0;
|
||||
double wire_x = 0.0;
|
||||
double wire_y = 0.0;
|
||||
double pan_x0 = 0.0;
|
||||
double pan_y0 = 0.0;
|
||||
/// Where the box being dragged *was* when the drag began, and which box it is. The gesture
|
||||
/// reports the total offset from its own start point, so adding it to a value that the previous
|
||||
/// update already moved is a runaway: the box accelerates away from the pointer and leaves the
|
||||
@@ -3549,25 +3560,34 @@ void math_canvas_draw(GtkDrawingArea*, cairo_t* cr, int width, int height, gpoin
|
||||
cairo_arc(cr, r.x + r.width - 12.0, r.y + 12.0, 4.0, 0.0, 2.0 * 3.14159265358979);
|
||||
cairo_fill(cr);
|
||||
}
|
||||
// Ports: inputs on the left, the output on the right.
|
||||
for (int k = 0; k < math_op_arity(n.op); ++k) {
|
||||
double px = 0.0;
|
||||
double py = 0.0;
|
||||
math_input_pos(n, k, px, py);
|
||||
const bool armed = d->pending_out >= 0 && static_cast<int>(i) != d->pending_out;
|
||||
cairo_set_source_rgb(cr, armed ? 1.0 : 0.45, armed ? 0.69 : 0.52, armed ? 0.13 : 0.62);
|
||||
cairo_set_source_rgb(cr, 0.45, 0.52, 0.62);
|
||||
cairo_rectangle(cr, px - kPort * 0.5, py - kPort * 0.5, kPort, kPort);
|
||||
cairo_fill(cr);
|
||||
}
|
||||
double ox = 0.0;
|
||||
double oy = 0.0;
|
||||
math_output_pos(n, ox, oy);
|
||||
cairo_set_source_rgb(cr, d->pending_out == static_cast<int>(i) ? 1.0 : 0.45,
|
||||
d->pending_out == static_cast<int>(i) ? 0.69 : 0.52,
|
||||
d->pending_out == static_cast<int>(i) ? 0.13 : 0.62);
|
||||
cairo_set_source_rgb(cr, 0.45, 0.52, 0.62);
|
||||
cairo_rectangle(cr, ox - kPort * 0.5, oy - kPort * 0.5, kPort, kPort);
|
||||
cairo_fill(cr);
|
||||
}
|
||||
// The wire being dragged, drawn last so it is over the boxes: without it the gesture gives no
|
||||
// sign of what it is about to connect.
|
||||
if (d->drag_mode == MathDrag::Wire && d->wire_from >= 0 &&
|
||||
d->wire_from < static_cast<int>(d->nodes.size())) {
|
||||
double ax = 0.0;
|
||||
double ay = 0.0;
|
||||
math_output_pos(d->nodes[static_cast<std::size_t>(d->wire_from)], ax, ay);
|
||||
cairo_set_source_rgb(cr, 1.0, 0.69, 0.13);
|
||||
cairo_set_line_width(cr, 1.5);
|
||||
cairo_move_to(cr, ax, ay);
|
||||
cairo_curve_to(cr, ax + 40.0, ay, d->wire_x - 40.0, d->wire_y, d->wire_x, d->wire_y);
|
||||
cairo_stroke(cr);
|
||||
}
|
||||
cairo_restore(cr);
|
||||
(void)width;
|
||||
(void)height;
|
||||
@@ -3579,60 +3599,135 @@ void math_canvas_pressed(GtkGestureClick*, int, double x, double y, gpointer use
|
||||
// and `Ctrl+0` to reach it.
|
||||
gtk_widget_grab_focus(d->canvas);
|
||||
int which = kPortOut;
|
||||
const int node = math_port_at_canvas(d, x, y, which);
|
||||
if (node >= 0 && which == kPortOut) {
|
||||
// An output square: arm it. Clicking it again disarms, so a mis-click needs no escape.
|
||||
d->pending_out = d->pending_out == node ? -1 : node;
|
||||
gtk_widget_queue_draw(d->canvas);
|
||||
const int port = math_port_at_canvas(d, x, y, which);
|
||||
if (port >= 0) {
|
||||
d->selected = port;
|
||||
math_dialog_sync(d);
|
||||
return;
|
||||
}
|
||||
const int node = math_node_at_canvas(d, x, y);
|
||||
if (node >= 0) {
|
||||
if (d->pending_out >= 0 && d->pending_out != node) {
|
||||
MathNode& n = d->nodes[static_cast<std::size_t>(node)];
|
||||
(which == 0 ? n.in_a : n.in_b) = d->pending_out;
|
||||
d->pending_out = -1;
|
||||
d->selected = node;
|
||||
math_dialog_sync(d);
|
||||
gtk_widget_queue_draw(d->canvas);
|
||||
return;
|
||||
}
|
||||
d->selected = node;
|
||||
math_dialog_sync(d);
|
||||
return;
|
||||
}
|
||||
const int body = math_node_at_canvas(d, x, y);
|
||||
if (body >= 0) {
|
||||
d->selected = body;
|
||||
d->pending_out = -1;
|
||||
math_dialog_sync(d);
|
||||
return;
|
||||
}
|
||||
d->pending_out = -1;
|
||||
gtk_widget_queue_draw(d->canvas);
|
||||
}
|
||||
|
||||
void math_canvas_drag_begin(GtkGestureDrag*, double x, double y, gpointer user) {
|
||||
MathDialog* d = static_cast<MathDialog*>(user);
|
||||
// Only a drag that *began on a box* moves one. Dragging the background used to move whatever was
|
||||
// selected, which reads as the window fighting the pointer.
|
||||
d->drag_mode = MathDrag::None;
|
||||
d->drag_node = -1;
|
||||
d->wire_from = -1;
|
||||
d->wire_detach = -1;
|
||||
// Precedence: a port, then a box, then the background. A port sticks out past the box edge, so
|
||||
// asking the box first would make the output square unreachable -- the same geometry that made
|
||||
// the old two-click protocol disarm itself.
|
||||
int which = kPortOut;
|
||||
const int port = math_port_at_canvas(d, x, y, which);
|
||||
if (port >= 0) {
|
||||
d->drag_mode = MathDrag::Wire;
|
||||
double gx = 0.0;
|
||||
double gy = 0.0;
|
||||
math_to_graph(d, x, y, gx, gy);
|
||||
d->wire_x = gx;
|
||||
d->wire_y = gy;
|
||||
if (which == kPortOut) {
|
||||
d->wire_from = port;
|
||||
} else {
|
||||
// A wired input is dragged *off*: the input is emptied now, so releasing over nothing is
|
||||
// itself the detach and needs no second verb. `Escape` puts it back.
|
||||
MathNode& n = d->nodes[static_cast<std::size_t>(port)];
|
||||
const int from = which == 0 ? n.in_a : n.in_b;
|
||||
if (from < 0) {
|
||||
d->drag_mode = MathDrag::None;
|
||||
return;
|
||||
}
|
||||
d->wire_from = from;
|
||||
d->wire_detach = port;
|
||||
d->wire_detach_which = which;
|
||||
(which == 0 ? n.in_a : n.in_b) = -1;
|
||||
}
|
||||
d->selected = d->wire_from;
|
||||
math_dialog_sync(d);
|
||||
gtk_widget_queue_draw(d->canvas);
|
||||
return;
|
||||
}
|
||||
const int node = math_node_at_canvas(d, x, y);
|
||||
if (node >= 0) {
|
||||
d->drag_mode = MathDrag::Node;
|
||||
d->drag_node = node;
|
||||
if (node < 0) return;
|
||||
d->selected = node;
|
||||
d->grab_x = d->nodes[static_cast<std::size_t>(node)].x;
|
||||
d->grab_y = d->nodes[static_cast<std::size_t>(node)].y;
|
||||
math_dialog_sync(d);
|
||||
return;
|
||||
}
|
||||
d->drag_mode = MathDrag::Pan;
|
||||
d->pan_x0 = d->view_x;
|
||||
d->pan_y0 = d->view_y;
|
||||
}
|
||||
|
||||
void math_canvas_drag(GtkGestureDrag*, double dx, double dy, gpointer user) {
|
||||
MathDialog* d = static_cast<MathDialog*>(user);
|
||||
// The gesture reports the *total* offset from where it began, so every branch adds it to a value
|
||||
// recorded at `drag-begin` and never to one a previous update wrote. Feeding a delta back into
|
||||
// the origin is what made a box fly off the canvas.
|
||||
switch (d->drag_mode) {
|
||||
case MathDrag::Node: {
|
||||
if (d->drag_node < 0 || d->drag_node >= static_cast<int>(d->nodes.size())) return;
|
||||
MathNode& n = d->nodes[static_cast<std::size_t>(d->drag_node)];
|
||||
// The *total* offset from where the box was when the drag began -- from `grab_x`/`grab_y`, which
|
||||
// no update writes. Feeding each update's delta back into the origin is what made the box fly off.
|
||||
// The gesture's offset is in canvas pixels; a node's position is in graph units.
|
||||
// The offset is in canvas pixels and a position is in graph units.
|
||||
n.x = std::max(0.0, d->grab_x + dx / d->view_scale);
|
||||
n.y = std::max(0.0, d->grab_y + dy / d->view_scale);
|
||||
break;
|
||||
}
|
||||
case MathDrag::Wire: {
|
||||
// The loose end, so the user can see what is being connected.
|
||||
double gx = 0.0;
|
||||
double gy = 0.0;
|
||||
math_to_graph(d, d->wire_x * d->view_scale + d->view_x + dx,
|
||||
d->wire_y * d->view_scale + d->view_y + dy, gx, gy);
|
||||
d->wire_x = gx;
|
||||
d->wire_y = gy;
|
||||
break;
|
||||
}
|
||||
case MathDrag::Pan:
|
||||
d->view_x = d->pan_x0 + dx;
|
||||
d->view_y = d->pan_y0 + dy;
|
||||
break;
|
||||
case MathDrag::None:
|
||||
return;
|
||||
}
|
||||
gtk_widget_queue_draw(d->canvas);
|
||||
}
|
||||
|
||||
void math_canvas_drag_end(GtkGestureDrag*, double dx, double dy, gpointer user) {
|
||||
MathDialog* d = static_cast<MathDialog*>(user);
|
||||
if (d->drag_mode != MathDrag::Wire) {
|
||||
d->drag_mode = MathDrag::None;
|
||||
d->drag_node = -1;
|
||||
return;
|
||||
}
|
||||
// Where the release landed, recomputed from the gesture's own offset rather than from `wire_x`:
|
||||
// `drag-end` carries the whole offset, and the update that would have moved `wire_x` to it is
|
||||
// not guaranteed to have arrived.
|
||||
double ox = 0.0;
|
||||
double oy = 0.0;
|
||||
math_output_pos(d->nodes[static_cast<std::size_t>(d->wire_from)], ox, oy);
|
||||
int which = kPortOut;
|
||||
const int target = math_port_at_canvas(d, ox * d->view_scale + d->view_x + dx,
|
||||
oy * d->view_scale + d->view_y + dy, which);
|
||||
if (target >= 0 && which != kPortOut && target != d->wire_from) {
|
||||
MathNode& n = d->nodes[static_cast<std::size_t>(target)];
|
||||
(which == 0 ? n.in_a : n.in_b) = d->wire_from;
|
||||
d->selected = target;
|
||||
math_dialog_sync(d);
|
||||
}
|
||||
// A release over nothing leaves the input the drag emptied empty: that *is* the detach.
|
||||
d->drag_mode = MathDrag::None;
|
||||
d->wire_from = -1;
|
||||
d->wire_detach = -1;
|
||||
gtk_widget_queue_draw(d->canvas);
|
||||
}
|
||||
|
||||
@@ -3671,6 +3766,20 @@ gboolean math_canvas_key(GtkEventControllerKey*, guint keyval, guint, GdkModifie
|
||||
math_view_fit(d);
|
||||
return TRUE;
|
||||
}
|
||||
if (keyval == GDK_KEY_Escape) {
|
||||
// A detach empties the input at `drag-begin`, so the undo is to put it back. Anything else is
|
||||
// a cancel of a gesture that has changed nothing yet.
|
||||
if (d->drag_mode == MathDrag::Wire && d->wire_detach >= 0 &&
|
||||
d->wire_detach < static_cast<int>(d->nodes.size())) {
|
||||
MathNode& n = d->nodes[static_cast<std::size_t>(d->wire_detach)];
|
||||
(d->wire_detach_which == 0 ? n.in_a : n.in_b) = d->wire_from;
|
||||
}
|
||||
d->drag_mode = MathDrag::None;
|
||||
d->wire_from = -1;
|
||||
d->wire_detach = -1;
|
||||
gtk_widget_queue_draw(d->canvas);
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
@@ -3717,7 +3826,9 @@ void on_math_node_deleted(GtkButton*, gpointer user) {
|
||||
if (d->output == gone) d->output = -1;
|
||||
else if (d->output > gone) d->output -= 1;
|
||||
d->selected = std::min<int>(gone, static_cast<int>(d->nodes.size()) - 1);
|
||||
d->pending_out = -1;
|
||||
d->drag_mode = MathDrag::None;
|
||||
d->wire_from = -1;
|
||||
d->wire_detach = -1;
|
||||
math_dialog_sync(d);
|
||||
}
|
||||
|
||||
@@ -4015,6 +4126,11 @@ void open_math_dialog(App& app, int edit) {
|
||||
math_canvas_drag(g, dx, dy, user);
|
||||
}),
|
||||
d);
|
||||
g_signal_connect(drag, "drag-end", G_CALLBACK(+[](GtkGestureDrag* g, double dx, double dy,
|
||||
gpointer user) {
|
||||
math_canvas_drag_end(g, dx, dy, user);
|
||||
}),
|
||||
d);
|
||||
gtk_widget_add_controller(d->canvas, GTK_EVENT_CONTROLLER(drag));
|
||||
|
||||
// The wheel zooms. A scroll controller is the only way to get one on a drawing area, and
|
||||
@@ -7028,6 +7144,71 @@ gboolean math_probe(gpointer user) {
|
||||
v->view_scale, v->view_x, v->view_y);
|
||||
}
|
||||
}
|
||||
if (a->math_dialog != nullptr) {
|
||||
auto* g = static_cast<MathDialog*>(
|
||||
g_object_get_data(G_OBJECT(a->math_dialog), "udpscope-math-dialog"));
|
||||
// The editor's two graphs are source -> gain, so node 1 has one input and node 0 an output.
|
||||
if (g != nullptr && g->nodes.size() >= 2) {
|
||||
math_view_reset(g);
|
||||
const auto canvas_of = [&](double gx, double gy, double& cx, double& cy) {
|
||||
cx = gx * g->view_scale + g->view_x;
|
||||
cy = gy * g->view_scale + g->view_y;
|
||||
};
|
||||
double ox = 0.0;
|
||||
double oy = 0.0;
|
||||
double ix = 0.0;
|
||||
double iy = 0.0;
|
||||
math_output_pos(g->nodes[0], ox, oy);
|
||||
math_input_pos(g->nodes[1], 0, ix, iy);
|
||||
double ocx = 0.0;
|
||||
double ocy = 0.0;
|
||||
double icx = 0.0;
|
||||
double icy = 0.0;
|
||||
canvas_of(ox, oy, ocx, ocy);
|
||||
canvas_of(ix, iy, icx, icy);
|
||||
|
||||
// Connect: press the output, release over the input.
|
||||
g->nodes[1].in_a = -1;
|
||||
math_canvas_drag_begin(nullptr, ocx, ocy, g);
|
||||
const int mode_wire = static_cast<int>(g->drag_mode);
|
||||
math_canvas_drag(nullptr, icx - ocx, icy - ocy, g);
|
||||
math_canvas_drag_end(nullptr, icx - ocx, icy - ocy, g);
|
||||
std::fprintf(stderr,
|
||||
"[math-probe] wire: connect mode=%d in_a=%d (want %d 0)\n", mode_wire,
|
||||
g->nodes[1].in_a, static_cast<int>(MathDrag::Wire));
|
||||
|
||||
// Detach: a drag that starts on a *wired* input empties it at once, so a release over
|
||||
// nothing is the detach. Dropping it back is what the connect above already covers.
|
||||
math_canvas_drag_begin(nullptr, icx, icy, g);
|
||||
const int emptied = g->nodes[1].in_a;
|
||||
math_canvas_drag(nullptr, 0.0, 90.0, g);
|
||||
math_canvas_drag_end(nullptr, 0.0, 90.0, g);
|
||||
std::fprintf(stderr,
|
||||
"[math-probe] detach: emptied_at_begin=%d in_a=%d (want -1 -1)\n", emptied,
|
||||
g->nodes[1].in_a);
|
||||
|
||||
// And Escape during the same gesture must put it back.
|
||||
g->nodes[1].in_a = 0;
|
||||
math_canvas_drag_begin(nullptr, icx, icy, g);
|
||||
math_canvas_drag(nullptr, 0.0, 90.0, g);
|
||||
math_canvas_key(nullptr, GDK_KEY_Escape, 0, static_cast<GdkModifierType>(0), g);
|
||||
std::fprintf(stderr, "[math-probe] detach: escape restores in_a=%d (want 0)\n",
|
||||
g->nodes[1].in_a);
|
||||
|
||||
// A drag that begins on the background pans and moves nothing.
|
||||
const double was_x = g->view_x;
|
||||
const double node_x = g->nodes[0].x;
|
||||
math_canvas_drag_begin(nullptr, 5.0, 5.0, g);
|
||||
const int mode_pan = static_cast<int>(g->drag_mode);
|
||||
math_canvas_drag(nullptr, 30.0, 0.0, g);
|
||||
math_canvas_drag_end(nullptr, 30.0, 0.0, g);
|
||||
std::fprintf(stderr,
|
||||
"[math-probe] view: pan mode=%d dview=%.1f dnode=%.1f (want %d 30.0 0.0)\n",
|
||||
mode_pan, g->view_x - was_x, g->nodes[0].x - node_x,
|
||||
static_cast<int>(MathDrag::Pan));
|
||||
math_view_reset(g);
|
||||
}
|
||||
}
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user