📄 ui/src/Views/GraphConnection.cs
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Cfa.Ui.Graph;

namespace Cfa.Ui.Views;

public sealed class GraphConnection : Control
{
    public static readonly StyledProperty<GraphEdge?> EdgeProperty = AvaloniaProperty.Register<
        GraphConnection,
        GraphEdge?
    >(nameof(Edge));

    static GraphConnection() => AffectsRender<GraphConnection>(EdgeProperty);

    public GraphEdge? Edge
    {
        get => GetValue(EdgeProperty);
        set => SetValue(EdgeProperty, value);
    }

    public override void Render(DrawingContext context)
    {
        base.Render(context);
        if (Edge is not { } edge)
        {
            return;
        }
        var start = edge.Start;
        var end = edge.End;
        var control1 = edge.Control1;
        var control2 = edge.Control2;
        var geometry = new StreamGeometry();
        using (var drawing = geometry.Open())
        {
            drawing.BeginFigure(start, false);
            drawing.CubicBezierTo(control1, control2, end);
            drawing.EndFigure(false);
        }
        var brush = Brushes.SkyBlue;
        context.DrawGeometry(null, new Pen(brush, edge.Thickness), geometry);
        var direction = new Vector(end.X - control2.X, end.Y - control2.Y);
        if (direction.Length > 0)
        {
            direction /= direction.Length;
            var normal = new Vector(-direction.Y, direction.X);
            var arrow = new StreamGeometry();
            using (var drawing = arrow.Open())
            {
                drawing.BeginFigure(end, true);
                drawing.LineTo(end - direction * 13 + normal * 6);
                drawing.LineTo(end - direction * 13 - normal * 6);
                drawing.EndFigure(true);
            }
            context.DrawGeometry(brush, null, arrow);
        }
    }
}