UI component for an editor wire (connection) in the Prism node graph. Draws the wire with themed colours, halo, endpoints, loss/dash/marker visuals, shows a tooltip describing source/target types and conversion, and handles gestures: double-click to insert a reroute, Ctrl+drag to duplicate (start a new drag from the source) while preserving normal drag semantics.
using Editor.Prism.Core;
using Editor.Prism.Model;
using Editor.Prism.Ui.Adapters;
using Connection = Editor.NodeEditor.Connection;
namespace Editor.Prism.Ui;
/// <summary>
/// A wire.
/// <para>
/// The built-in connection paints a single four-pixel stroke and switches to a hardcoded pink when
/// selected or hovered. Prism paints the source port's <em>resolved</em> type colour over a
/// canvas-coloured halo, so a dense graph stays readable where wires cross, and marks the two things
/// the built-in editor hides: a lossy or padded conversion gets a diamond at its midpoint with the
/// exact conversion in its tooltip, and an illegal one is drawn dashed in the error colour ending in an
/// X rather than silently truncating at compile time.
/// </para>
/// <para>
/// Two gestures are added on top of the framework's own: a double click inserts a reroute at the point
/// clicked, and Ctrl+drag pulls a <em>second</em> wire out of the same output instead of moving this
/// one. Shift+click still reroutes, and a plain drag still relocates the wire's input end.
/// </para>
/// </summary>
public class PrismConnection : Connection
{
/// <summary>How long two presses may be apart and still count as a double-click.</summary>
const float DoubleClickSeconds = 0.4f;
/// <summary>How far the pointer may travel between them.</summary>
const float DoubleClickSlop = 6f;
RealTimeSince _sinceLastPress = 1000f;
Vector2 _lastPress;
bool _duplicateArmed;
bool _duplicating;
/// <summary>Build a wire between two sockets.</summary>
public PrismConnection( PlugOut output, PlugIn input ) : base( output, input )
{
ZIndex = -10;
HoverEvents = true;
Selectable = true;
Cursor = CursorShape.Finger;
}
/// <summary>The Prism output plug this wire leaves, when there is one.</summary>
public PrismPlugOut Source => Output.IsValid() ? Output.Inner as PrismPlugOut : null;
/// <summary>The Prism input plug this wire enters, when there is one.</summary>
public PrismPlugIn Target => Input.IsValid() ? Input.Inner as PrismPlugIn : null;
/// <summary>The type flowing down the wire.</summary>
public ShaderType SourceType => Source?.EffectiveType ?? ShaderType.Void;
/// <summary>The type the wire has to arrive as.</summary>
public ShaderType TargetType => Target?.EffectiveType ?? ShaderType.Void;
/// <summary>What the compiler will have to do to get one type into the other.</summary>
public ConversionKind Conversion
{
get
{
var from = SourceType;
var to = TargetType;
if ( from.IsVoid || to.IsVoid ) return ConversionKind.Identity;
return TypeRules.Classify( from, to );
}
}
/// <summary>True when the conversion this wire implies is not legal at all.</summary>
public bool IsBroken => Conversion == ConversionKind.Illegal;
/// <summary>True when the conversion costs information or invents it.</summary>
public bool IsLossy => TypeRules.IsLossy( Conversion );
/// <summary>The wire's colour before selection and reachability are taken into account.</summary>
public Color BaseColor
{
get
{
if ( IsBroken ) return PrismTheme.Error;
var plug = Source ?? (PrismPlug)Target;
return plug?.TypeColor ?? PrismTheme.TypeGeneric;
}
}
/// <inheritdoc/>
protected override void OnPaint()
{
if ( !Output.IsValid() && !Input.IsValid() ) return;
// The base hides itself while the wire is being re-dragged; its flag is private, but it also
// swaps the cursor, which is observable.
if ( Cursor == CursorShape.DragLink ) return;
Paint.Antialiasing = true;
var selected = Paint.HasSelected;
var hovered = Paint.HasMouseOver;
var color = BaseColor;
if ( ColorTint.a > 0f ) color = Color.Lerp( color, ColorTint.WithAlpha( 1f ), ColorTint.a );
var reachable = ( Output.IsValid() && Output.Node.Node.IsReachable )
|| ( Input.IsValid() && Input.Node.Node.IsReachable );
if ( !reachable ) color = color.Desaturate( 0.55f ).Darken( 0.3f );
if ( selected ) color = PrismTheme.Accent;
var width = ( hovered ? PrismTheme.WireWidthHover : PrismTheme.WireWidth ) * MathF.Max( 0.1f, WidthScale );
if ( selected )
{
// The framework's selected wire is a hardcoded pink; ours is the theme accent with a glow.
Paint.ClearBrush();
Paint.SetPen( PrismTheme.Accent.WithAlpha( 0.2f ), width + 6f );
PaintLine();
}
Paint.ClearBrush();
Paint.SetPen( PrismTheme.Canvas.WithAlpha( 0.85f ), width + PrismTheme.WireHaloWidth - PrismTheme.WireWidth );
PaintLine();
Paint.SetPen( color, width, IsBroken ? PenStyle.Dash : PenStyle.Solid );
PaintLine();
PaintEndpoints( color );
PaintMarker( color );
}
void PaintEndpoints( Color color )
{
if ( Output.IsValid() )
{
var point = FromScene( OutputPosition );
PrismPaint.Dot( point, PrismTheme.WireEndpointRadius * 0.5f + 1f, PrismTheme.Canvas );
PrismPaint.Dot( point, PrismTheme.WireEndpointRadius * 0.5f, color );
}
if ( !Input.IsValid() ) return;
var target = FromScene( InputPosition );
if ( IsBroken )
{
PrismPaint.Cross( target, 4f, PrismTheme.Error );
return;
}
PrismPaint.Dot( target, PrismTheme.WireEndpointRadius * 0.5f + 1f, PrismTheme.Canvas );
PrismPaint.Dot( target, PrismTheme.WireEndpointRadius * 0.5f, color );
}
void PaintMarker( Color color )
{
if ( !IsLossy ) return;
if ( !Output.IsValid() || !Input.IsValid() ) return;
var mid = FromScene( ( OutputPosition + InputPosition ) * 0.5f );
PrismPaint.Diamond( mid, 4.5f, PrismTheme.Warning, PrismTheme.Canvas );
}
/// <summary>The document edge this wire draws, when it is a real connection rather than a preview.</summary>
public bool TryGetEdge( out Edge edge )
{
edge = null;
var target = Target;
var node = target?.Owner?.PrismNode;
var document = target?.Owner?.Adapter?.Document;
if ( node is null || document is null ) return false;
return document.TryGetIncomingEdge( node.Id, target.PortId, out edge );
}
/// <summary>
/// Insert a reroute in the middle of this wire.
/// <para>
/// Reimplemented rather than delegated because <c>GraphView.RerouteConnection</c> — what the built-in
/// SHIFT+click gesture calls — is <c>internal</c> and unreachable from an addon assembly. Going
/// through <c>GraphMutations.InsertReroute</c> instead also means the split is one undo step and the
/// edge ids on both halves are minted the same way every other edge is.
/// </para>
/// </summary>
public bool InsertReroute( Vector2 scenePosition )
{
if ( !TryGetEdge( out var edge ) ) return false;
var mutations = Target?.Owner?.Adapter?.Mutations;
if ( mutations is null ) return false;
return PrismLog.Guard( "Insert a reroute into a wire",
() => mutations.InsertReroute( edge.Id, scenePosition ) is not null, false );
}
/// <inheritdoc/>
protected override void OnMousePressed( GraphicsMouseEvent e )
{
// Ctrl+press only *arms* the duplicate; the gesture is taken over on the first move. Nothing is
// accepted here on purpose, so Qt's own selectable-item handling still runs and Ctrl+click keeps
// working as additive selection when the pointer never moves.
_duplicateArmed = e.LeftMouseButton && e.HasCtrl && !e.HasShift && !e.HasAlt && Output.IsValid();
if ( _duplicateArmed ) return;
if ( e.LeftMouseButton && !e.HasShift && !e.HasCtrl && !e.HasAlt )
{
var quick = _sinceLastPress < DoubleClickSeconds
&& ( e.ScenePosition - _lastPress ).Length < DoubleClickSlop;
_sinceLastPress = 0f;
_lastPress = e.ScenePosition;
// There is no double-click event on a graphics item, so the second press inside the window
// is what we act on. Not calling base is deliberate: base re-drags the wire's output end,
// and the user asked for a reroute, not a re-drag.
if ( quick && InsertReroute( e.ScenePosition ) )
{
_sinceLastPress = 1000f;
e.Accepted = true;
return;
}
}
base.OnMousePressed( e );
}
/// <summary>
/// Pull a second wire out of this one's source instead of moving it.
/// <para>
/// The framework's own drag on a wire re-drags its <em>output</em> end, which relocates the
/// connection. Ctrl instead forwards the gesture straight to the source plug through <c>Plug</c>'s
/// public event relays — which exist precisely so one item can drive another's gesture — so the
/// framework sees an ordinary drag begun at that output: it opens a preview wire, tracks a drop
/// target, and on release creates a new connection under a <c>"Create Connection"</c> undo step. The
/// original wire is never told anything happened, which is exactly what "duplicate" means.
/// </para>
/// </summary>
protected override void OnMouseMove( GraphicsMouseEvent e )
{
if ( _duplicateArmed )
{
if ( !Output.IsValid() )
{
_duplicateArmed = false;
_duplicating = false;
return;
}
_duplicating = true;
Output.MouseMove( e );
e.Accepted = true;
return;
}
base.OnMouseMove( e );
}
/// <inheritdoc/>
protected override void OnMouseReleased( GraphicsMouseEvent e )
{
var duplicating = _duplicating;
_duplicateArmed = false;
_duplicating = false;
if ( duplicating )
{
// Drops the preview wire onto whatever is under the cursor, or opens the create-node menu
// filtered to this output when there is nothing there — the same two outcomes a drag from
// the socket itself would have had.
if ( Output.IsValid() ) Output.MouseReleased( e );
e.Accepted = true;
return;
}
base.OnMouseReleased( e );
}
/// <inheritdoc/>
protected override void OnHoverEnter( GraphicsHoverEvent e )
{
base.OnHoverEnter( e );
var source = Source;
var target = Target;
if ( source is null || target is null ) return;
var from = SourceType;
var to = TargetType;
var conversion = Conversion;
var text = $"<span style=\"white-space: nowrap;\">" +
$"<b>{PrismShaderTypes.Name( from, source.IsColor )}</b> → " +
$"<b>{PrismShaderTypes.Name( to, target.IsColor )}</b><br/>" +
$"{source.Owner?.DisplayInfo.Name} › {source.Label} → " +
$"{target.Owner?.DisplayInfo.Name} › {target.Label}";
if ( conversion != ConversionKind.Identity && !from.IsVoid && !to.IsVoid )
{
var color = conversion == ConversionKind.Illegal ? PrismTheme.Error
: TypeRules.IsLossy( conversion ) ? PrismTheme.Warning
: PrismTheme.TextMuted;
text += $"<br/><span style=\"color: {color.Hex};\">{TypeRules.Describe( from, to, conversion )}</span>";
}
ToolTip = text + "</span>";
}
}