Editor UI adapters that replace framework node graph plugs with Prism-specific plug items implementing a screen-space drag threshold. PrismPlugItems.Upgrade swaps framework PlugIn/PlugOut instances with PrismPlugInItem/PrismPlugOutItem when safe. PrismPlugInItem and PrismPlugOutItem delay forwarding press events until the pointer has moved at least DragThreshold pixels, so clicks do not spawn ghost wires.
namespace Editor.Prism.Ui.Adapters;
/// <summary>
/// The socket graphics items Prism uses in place of the framework's own.
/// <para>
/// The node-graph library starts a wire drag on the <em>first pixel</em> of pointer movement over a
/// plug — its own source carries a <c>// TODO - minimum distance move</c> where the threshold should
/// be. The consequences are visible every session: a plain click on a handle spawns a ghost wire, and
/// releasing it a pixel later drops it on nothing, which pops the create-node menu; and a click on a
/// <em>connected</em> input tears the wire off before the user has moved at all.
/// </para>
/// <para>
/// <c>NodeUI.UpdatePlugs</c> is private and hardcodes <c>new PlugIn(...)</c> / <c>new PlugOut(...)</c>,
/// so a subclass cannot be injected at creation time — but <c>NodeUI.Inputs</c> and
/// <c>NodeUI.Outputs</c> are public lists and both plug constructors are public, so a freshly created
/// plug can be swapped for one of these the moment it appears. <see cref="Upgrade"/> does that, from
/// <c>PrismNodeUi.Layout()</c>, which the framework calls at the end of every <c>UpdatePlugs</c> pass —
/// before anything has had a chance to connect to the plug it replaces.
/// </para>
/// </summary>
public static class PrismPlugItems
{
/// <summary>
/// How far the pointer must travel, in screen pixels, before a press on a handle becomes a wire drag.
/// Measured on screen rather than in the scene so the gesture feels identical at every zoom level.
/// </summary>
public const float DragThreshold = 4f;
/// <summary>
/// True when a press is one the framework's plug would have swallowed and turned into a drag. Any
/// other press — right button, Ctrl, Shift — is left entirely alone so the framework's own gestures
/// (marquee from a plug, additive selection) keep working exactly as they did.
/// </summary>
internal static bool IsDragPress( in GraphicsMouseEvent e ) =>
e.LeftMouseButton && !e.HasCtrl && !e.HasShift;
/// <summary>True when the pointer has moved far enough from the press for this to be a drag.</summary>
internal static bool Crossed( Vector2 pressScreen, Vector2 nowScreen ) =>
( nowScreen - pressScreen ).Length >= DragThreshold;
/// <summary>
/// Replace every framework-created plug on a card with the Prism equivalent.
/// <para>
/// Connected plugs are left alone: a <c>Connection</c> holds its endpoints by reference, so swapping
/// one out from under a live wire would orphan it. In practice this never bites — a plug is upgraded
/// on the same call that created it, long before a wire can reach it — and the guard means the worst
/// case is a socket that keeps the stock behaviour rather than a canvas that loses a connection.
/// </para>
/// </summary>
/// <returns>How many plugs were replaced.</returns>
public static int Upgrade( NodeUI card )
{
if ( card is null || !card.IsValid ) return 0;
var upgraded = 0;
for ( int i = 0; i < card.Inputs.Count; i++ )
{
var plug = card.Inputs[i];
if ( plug is PrismPlugInItem || !plug.IsValid() || plug.IsConnected ) continue;
card.Inputs[i] = new PrismPlugInItem( card, plug.Inner );
plug.Destroy();
upgraded++;
}
for ( int i = 0; i < card.Outputs.Count; i++ )
{
var plug = card.Outputs[i];
if ( plug is PrismPlugOutItem || !plug.IsValid() || plug.IsConnected ) continue;
card.Outputs[i] = new PrismPlugOutItem( card, plug.Inner );
plug.Destroy();
upgraded++;
}
return upgraded;
}
}
/// <summary>
/// An input socket with a drag threshold.
/// <para>
/// The press is held back rather than forwarded, so nothing at all happens until the pointer has
/// travelled <see cref="PrismPlugItems.DragThreshold"/>. Only then is the base's press replayed — which
/// is the call that detaches an existing wire and starts re-dragging it — followed immediately by the
/// move that provoked it. A press that never crosses the threshold is simply a click on a handle, and
/// leaves both the canvas and the document exactly as they were.
/// </para>
/// </summary>
public sealed class PrismPlugInItem : PlugIn
{
Vector2 _pressScreen;
bool _armed;
bool _dragging;
/// <summary>Build an input socket for a card.</summary>
public PrismPlugInItem( NodeUI node, IPlug plug ) : base( node, plug ) { }
/// <summary>True while a press is being held below the drag threshold.</summary>
public bool IsArmed => _armed && !_dragging;
/// <inheritdoc/>
protected override void OnMousePressed( GraphicsMouseEvent e )
{
if ( PrismPlugItems.IsDragPress( e ) )
{
_pressScreen = e.ScreenPosition;
_armed = true;
_dragging = false;
// Accepted for exactly the same reason the base accepts it: to keep the grab, so the moves
// and the release arrive here and not on the card underneath.
e.Accepted = true;
return;
}
base.OnMousePressed( e );
}
/// <inheritdoc/>
protected override void OnMouseMove( GraphicsMouseEvent e )
{
if ( _armed && !_dragging )
{
if ( !PrismPlugItems.Crossed( _pressScreen, e.ScreenPosition ) )
{
e.Accepted = true;
return;
}
_dragging = true;
// Replay the press the gate withheld. This is what picks an existing wire up.
base.OnMousePressed( e );
}
base.OnMouseMove( e );
}
/// <inheritdoc/>
protected override void OnMouseReleased( GraphicsMouseEvent e )
{
var wasClick = _armed && !_dragging;
_armed = false;
_dragging = false;
// Still called on a click: with no drag in flight the base only resets the cursor and repaints,
// and skipping it would leave a stale DragLink cursor behind on the very next real drag.
base.OnMouseReleased( e );
if ( wasClick ) e.Accepted = true;
}
}
/// <summary>
/// An output socket with a drag threshold. Same gate as <see cref="PrismPlugInItem"/>; an output has no
/// wire to detach, so the only thing the threshold buys here is that a click stops producing a ghost
/// wire and a create-node menu nobody asked for.
/// </summary>
public sealed class PrismPlugOutItem : PlugOut
{
Vector2 _pressScreen;
bool _armed;
bool _dragging;
/// <summary>Build an output socket for a card.</summary>
public PrismPlugOutItem( NodeUI node, IPlug plug ) : base( node, plug ) { }
/// <summary>True while a press is being held below the drag threshold.</summary>
public bool IsArmed => _armed && !_dragging;
/// <inheritdoc/>
protected override void OnMousePressed( GraphicsMouseEvent e )
{
if ( PrismPlugItems.IsDragPress( e ) )
{
_pressScreen = e.ScreenPosition;
_armed = true;
_dragging = false;
e.Accepted = true;
return;
}
base.OnMousePressed( e );
}
/// <inheritdoc/>
protected override void OnMouseMove( GraphicsMouseEvent e )
{
if ( _armed && !_dragging )
{
if ( !PrismPlugItems.Crossed( _pressScreen, e.ScreenPosition ) )
{
e.Accepted = true;
return;
}
_dragging = true;
base.OnMousePressed( e );
}
base.OnMouseMove( e );
}
/// <inheritdoc/>
protected override void OnMouseReleased( GraphicsMouseEvent e )
{
var wasClick = _armed && !_dragging;
_armed = false;
_dragging = false;
base.OnMouseReleased( e );
if ( wasClick ) e.Accepted = true;
}
}