| 1 | using System.Globalization; |
| 2 | using Avalonia; |
| 3 | using Avalonia.Media; |
| 4 | using CascadeIDE.Cockpit.PrimitivesKit; |
| 5 | using CascadeIDE.Features.WorkspaceNavigation.Application; |
| 6 | |
| 7 | namespace CascadeIDE.Services; |
| 8 | |
| 9 | /// <summary> |
| 10 | /// Отрисовка узлов control-flow в редакторе — те же заливки/обводки, что <see cref="Views.SkiaKit.Graph.SkiaGraphSceneDrawing"/> (палитра CFG). |
| 11 | /// </summary> |
| 12 | public static class ControlFlowEditorNodePainter |
| 13 | { |
| 14 | private const string ExitStepKind = "exit_step"; |
| 15 | private const string ConditionStepKind = "condition_step"; |
| 16 | private const string HandlerStepKind = "handler_step"; |
| 17 | |
| 18 | private static readonly IBrush AnchorFill = Brush(CockpitPrimitivesPalette.CodeNavigationMap.AnchorFill); |
| 19 | private static readonly IBrush ConditionFill = Brush(CockpitPrimitivesPalette.CodeNavigationMap.ConditionFill); |
| 20 | private static readonly IBrush ExitFill = Brush(CockpitPrimitivesPalette.CodeNavigationMap.ExitFill); |
| 21 | private static readonly IBrush CallFill = Brush(CockpitPrimitivesPalette.CodeNavigationMap.CallFill); |
| 22 | private static readonly IBrush HandlerFill = Brush(CockpitPrimitivesPalette.CodeNavigationMap.HandlerFill); |
| 23 | private static readonly Pen NodeStroke = new(new SolidColorBrush(CockpitPrimitivesPalette.CodeNavigationMap.NodeStroke), 1); |
| 24 | private static readonly IBrush GlyphOnFill = Brushes.White; |
| 25 | private static readonly IBrush GlyphOnExit = new SolidColorBrush(Color.FromRgb(220, 228, 240)); |
| 26 | |
| 27 | public static IBrush ResolveFillBrush(string nodeKind, ControlFlowNodeVisualKind visualKind) |
| 28 | { |
| 29 | if (visualKind == ControlFlowNodeVisualKind.Anchor) |
| 30 | return AnchorFill; |
| 31 | if (visualKind == ControlFlowNodeVisualKind.Diamond |
| 32 | || IsConditionKind(nodeKind)) |
| 33 | return ConditionFill; |
| 34 | if (visualKind == ControlFlowNodeVisualKind.Exit || IsExitKind(nodeKind)) |
| 35 | return ExitFill; |
| 36 | if (IsHandlerKind(nodeKind)) |
| 37 | return HandlerFill; |
| 38 | return CallFill; |
| 39 | } |
| 40 | |
| 41 | public static IBrush ResolveGlyphBrush(ControlFlowNodeVisualKind visualKind) => |
| 42 | visualKind == ControlFlowNodeVisualKind.Exit ? GlyphOnExit : GlyphOnFill; |
| 43 | |
| 44 | public static Pen NodeStrokePen => NodeStroke; |
| 45 | |
| 46 | public static IBrush NodeStrokeBrush => NodeStroke.Brush ?? Brushes.Black; |
| 47 | |
| 48 | public static void DrawNode( |
| 49 | DrawingContext ctx, |
| 50 | ControlFlowLineVisual visual, |
| 51 | double centerX, |
| 52 | double centerY, |
| 53 | double radius, |
| 54 | Typeface typeface, |
| 55 | double glyphFontSize) |
| 56 | { |
| 57 | var fill = ResolveFillBrush(visual.NodeKind, visual.VisualKind); |
| 58 | switch (visual.VisualKind) |
| 59 | { |
| 60 | case ControlFlowNodeVisualKind.Anchor: |
| 61 | ctx.DrawEllipse(fill, NodeStroke, new Rect(centerX - radius, centerY - radius, radius * 2, radius * 2)); |
| 62 | break; |
| 63 | case ControlFlowNodeVisualKind.Diamond: |
| 64 | DrawDiamond(ctx, centerX, centerY, radius, fill, NodeStroke); |
| 65 | break; |
| 66 | case ControlFlowNodeVisualKind.Exit: |
| 67 | ctx.DrawEllipse(fill, NodeStroke, new Rect(centerX - radius, centerY - radius, radius * 2, radius * 2)); |
| 68 | if (visual.ShowExitArrow) |
| 69 | DrawNeArrow(ctx, centerX, centerY, radius * 0.85); |
| 70 | DrawGlyphIfAny(ctx, visual, centerX, centerY, typeface, glyphFontSize); |
| 71 | return; |
| 72 | case ControlFlowNodeVisualKind.Circle: |
| 73 | ctx.DrawEllipse(fill, NodeStroke, new Rect(centerX - radius, centerY - radius, radius * 2, radius * 2)); |
| 74 | break; |
| 75 | } |
| 76 | |
| 77 | DrawGlyphIfAny(ctx, visual, centerX, centerY, typeface, glyphFontSize); |
| 78 | } |
| 79 | |
| 80 | private static void DrawGlyphIfAny( |
| 81 | DrawingContext ctx, |
| 82 | ControlFlowLineVisual visual, |
| 83 | double cx, |
| 84 | double cy, |
| 85 | Typeface typeface, |
| 86 | double fontSize) |
| 87 | { |
| 88 | if (string.IsNullOrEmpty(visual.TextGlyph)) |
| 89 | return; |
| 90 | |
| 91 | var ft = new FormattedText( |
| 92 | visual.TextGlyph, |
| 93 | CultureInfo.InvariantCulture, |
| 94 | FlowDirection.LeftToRight, |
| 95 | typeface, |
| 96 | fontSize, |
| 97 | ResolveGlyphBrush(visual.VisualKind)); |
| 98 | ctx.DrawText(ft, new Point(cx - ft.Width / 2, cy - ft.Height / 2)); |
| 99 | } |
| 100 | |
| 101 | private static void DrawDiamond(DrawingContext ctx, double cx, double cy, double r, IBrush fill, Pen pen) |
| 102 | { |
| 103 | var geo = new StreamGeometry(); |
| 104 | using (var ig = geo.Open()) |
| 105 | { |
| 106 | ig.BeginFigure(new Point(cx, cy - r), true); |
| 107 | ig.LineTo(new Point(cx + r, cy)); |
| 108 | ig.LineTo(new Point(cx, cy + r)); |
| 109 | ig.LineTo(new Point(cx - r, cy)); |
| 110 | ig.EndFigure(true); |
| 111 | } |
| 112 | |
| 113 | ctx.DrawGeometry(fill, pen, geo); |
| 114 | } |
| 115 | |
| 116 | private static void DrawNeArrow(DrawingContext ctx, double cx, double cy, double len) |
| 117 | { |
| 118 | var pen = new Pen(GlyphOnExit, 1.15) { LineCap = PenLineCap.Round }; |
| 119 | const double dirX = 0.70710678118654757; |
| 120 | const double dirY = -0.70710678118654757; |
| 121 | var tip = new Point(cx + dirX * (len * 0.35), cy + dirY * (len * 0.35)); |
| 122 | var basePt = new Point(tip.X - dirX * len, tip.Y - dirY * len); |
| 123 | ctx.DrawLine(pen, basePt, tip); |
| 124 | var wing = len * 0.38; |
| 125 | var px = -dirY; |
| 126 | var py = dirX; |
| 127 | ctx.DrawLine( |
| 128 | pen, |
| 129 | new Point(tip.X - dirX * wing + px * wing * 0.35, tip.Y - dirY * wing + py * wing * 0.35), |
| 130 | tip); |
| 131 | ctx.DrawLine( |
| 132 | pen, |
| 133 | new Point(tip.X - dirX * wing - px * wing * 0.35, tip.Y - dirY * wing - py * wing * 0.35), |
| 134 | tip); |
| 135 | } |
| 136 | |
| 137 | private static bool IsExitKind(string kind) => |
| 138 | string.Equals(kind, ExitStepKind, StringComparison.OrdinalIgnoreCase); |
| 139 | |
| 140 | private static bool IsConditionKind(string kind) => |
| 141 | string.Equals(kind, ConditionStepKind, StringComparison.OrdinalIgnoreCase); |
| 142 | |
| 143 | private static bool IsHandlerKind(string kind) => |
| 144 | string.Equals(kind, HandlerStepKind, StringComparison.OrdinalIgnoreCase); |
| 145 | |
| 146 | private static SolidColorBrush Brush(Color c) => new(c); |
| 147 | } |
| 148 | |