Forge
csharpdeeb25a2
1using System.Collections.Generic;
2using System.Globalization;
3using Avalonia;
4using Avalonia.Media;
5using CascadeIDE.Cockpit.Graph.Layout;
6
7namespace CascadeIDE.Views.SkiaKit.Graph;
8
9public static partial class SkiaGraphSceneDrawing
10{
11 private static void DrawLegend(DrawingContext context, GraphLayoutScene scene, SkiaGraphVisualTheme theme, double w, double h)
12 {
13 if (!scene.UseLegendColumn || h < 40)
14 return;
15
16 var isBelow = scene.LegendPlacement == GraphLegendBlockPlacement.BelowGraph;
17 if (isBelow)
18 {
19 if (scene.LegendBlockTopY <= 0 || scene.LegendBlockTopY >= h - 12)
20 return;
21 }
22 else if (scene.LegendColumnLeft >= w - 24)
23 {
24 return;
25 }
26
27 var x0 = scene.LegendColumnLeft;
28 var y = isBelow ? scene.LegendBlockTopY : 8d;
29 var legendViewportH = h - y - 4;
30 if (legendViewportH < 12)
31 return;
32
33 var captionSize = scene.SideLabelFontSizePx is { } s
34 ? Math.Clamp(s, SkiaGraphRenderInvariants.MinLegendCaptionFontSize, SkiaGraphRenderInvariants.MaxSideLabelFontSize)
35 : 12;
36
37 var idxColW = MeasureIndexColumnWidth(theme, scene.Legend, captionSize);
38 const double colGap = 6d;
39 var textX = x0 + idxColW + colGap;
40 var textMaxW = Math.Max(24, w - textX - 4);
41
42 captionSize = FitLegendCaptionSize(theme, scene.Legend, textMaxW, captionSize, legendViewportH);
43
44 var lineH = Math.Max(15, captionSize * 1.2);
45 var keyRowH = Math.Max(17d, captionSize + 5);
46 const double gapBeforeKeys = 6d;
47
48 foreach (var row in scene.Legend)
49 {
50 if (y + lineH > h - 4)
51 return;
52 var idxTxt = row.Index.ToString(CultureInfo.InvariantCulture);
53 var idxFt = new FormattedText(
54 idxTxt,
55 CultureInfo.InvariantCulture,
56 FlowDirection.LeftToRight,
57 theme.SideLabelTypeface,
58 captionSize,
59 theme.SideLabelBrush);
60 context.DrawText(idxFt, new Point(x0 + idxColW - idxFt.Width, y));
61
62 var body = row.Text.Replace('\r', ' ').Replace('\n', ' ');
63 while (body.Contains(" ", StringComparison.Ordinal))
64 body = body.Replace(" ", " ", StringComparison.Ordinal);
65 body = body.Trim();
66 if (body.Length > 400)
67 body = body[..397] + "…";
68
69 var bodyFt = new FormattedText(
70 body,
71 CultureInfo.InvariantCulture,
72 FlowDirection.LeftToRight,
73 theme.SideLabelTypeface,
74 captionSize,
75 theme.SideLabelBrush);
76 context.DrawText(bodyFt, new Point(textX, y));
77 y += lineH;
78 }
79
80 var hasShapeKeys = scene.ShowLegendReturnKey || scene.ShowLegendConditionKey || scene.ShowLegendExceptionFlowKey;
81 var hasAnyKeys = hasShapeKeys || scene.ShowLegendEdgeStyleKey;
82 if (!hasAnyKeys)
83 return;
84
85 if (scene.Legend.Count > 0 && (scene.ShowLegendEdgeStyleKey || hasShapeKeys))
86 y += gapBeforeKeys;
87
88 if (scene.ShowLegendEdgeStyleKey)
89 {
90 y = DrawLegendEdgeStyleKeyBlock(
91 context,
92 theme,
93 x0,
94 y,
95 keyRowH,
96 captionSize,
97 h,
98 scene.Edges);
99 }
100
101 if (hasShapeKeys && scene.ShowLegendEdgeStyleKey)
102 y += gapBeforeKeys;
103
104 if (scene.ShowLegendReturnKey)
105 {
106 if (y + keyRowH > h - 4)
107 return;
108 DrawLegendReturnKeyRow(context, theme, x0, y, keyRowH, captionSize);
109 y += keyRowH + 2;
110 }
111
112 if (scene.ShowLegendConditionKey)
113 {
114 if (y + keyRowH > h - 4)
115 return;
116 DrawLegendConditionKeyRow(context, theme, x0, y, keyRowH, captionSize);
117 y += keyRowH + 2;
118 }
119
120 if (scene.ShowLegendExceptionFlowKey)
121 {
122 if (y + keyRowH > h - 4)
123 return;
124 DrawLegendExceptionFlowKeyRow(context, theme, x0, y, keyRowH, captionSize);
125 }
126 }
127
128 private static double MeasureIndexColumnWidth(SkiaGraphVisualTheme theme, IReadOnlyList<GraphLegendEntry> rows, double captionSize)
129 {
130 var idxColW = 0d;
131 foreach (var row in rows)
132 {
133 var idxTxt = row.Index.ToString(CultureInfo.InvariantCulture);
134 var idxFt = new FormattedText(
135 idxTxt,
136 CultureInfo.InvariantCulture,
137 FlowDirection.LeftToRight,
138 theme.SideLabelTypeface,
139 captionSize,
140 theme.SideLabelBrush);
141 idxColW = Math.Max(idxColW, idxFt.Width);
142 }
143
144 return idxColW + 4;
145 }
146
147 private static double FitLegendCaptionSize(
148 SkiaGraphVisualTheme theme,
149 IReadOnlyList<GraphLegendEntry> rows,
150 double textMaxW,
151 double captionSize,
152 double viewportH)
153 {
154 var size = captionSize;
155 const double floor = 9;
156 while (size >= floor)
157 {
158 var lineH = Math.Max(15, size * 1.2);
159 var used = 8d + rows.Count * lineH;
160 if (used > viewportH - 8 && rows.Count > 0)
161 {
162 size -= 0.5;
163 continue;
164 }
165
166 var fits = true;
167 foreach (var row in rows)
168 {
169 var t = row.Text.Replace('\r', ' ').Replace('\n', ' ');
170 while (t.Contains(" ", StringComparison.Ordinal))
171 t = t.Replace(" ", " ", StringComparison.Ordinal);
172 t = t.Trim();
173 if (t.Length > 400)
174 t = t[..397] + "…";
175 var w = new FormattedText(
176 t,
177 CultureInfo.InvariantCulture,
178 FlowDirection.LeftToRight,
179 theme.SideLabelTypeface,
180 size,
181 theme.SideLabelBrush).Width;
182 if (w > textMaxW)
183 {
184 fits = false;
185 break;
186 }
187 }
188
189 if (fits)
190 return size;
191 size -= 0.5;
192 }
193
194 return floor;
195 }
196
197 private static void DrawLegendReturnKeyRow(DrawingContext context, SkiaGraphVisualTheme theme, double x0, double y, double rowH, double captionSize)
198 {
199 const double iconR = 5.5;
200 var cy = y + rowH / 2;
201 var cx = x0 + iconR + 1;
202 context.DrawEllipse(theme.ExitFill, theme.NodeStrokePen, new Point(cx, cy), iconR, iconR);
203 var arrowLen = Math.Max(4.5, Math.Min(iconR * 1.35, captionSize * 0.55));
204 DrawNorthEastExitArrowShaftCentered(context, theme.GlyphBrush, new Point(cx, cy), arrowLen, 1.2);
205
206 var cap = new FormattedText(
207 "return",
208 CultureInfo.InvariantCulture,
209 FlowDirection.LeftToRight,
210 theme.SideLabelTypeface,
211 captionSize,
212 theme.SideLabelBrush);
213 context.DrawText(cap, new Point(x0 + iconR * 2 + 10, y + (rowH - cap.Height) / 2));
214 }
215
216 private static void DrawLegendConditionKeyRow(DrawingContext context, SkiaGraphVisualTheme theme, double x0, double y, double rowH, double captionSize)
217 {
218 const double r = 5.5;
219 var cy = y + rowH / 2;
220 var cx = x0 + r + 1;
221 var geo = new StreamGeometry();
222 using (var ig = geo.Open())
223 {
224 ig.BeginFigure(new Point(cx, cy - r), true);
225 ig.LineTo(new Point(cx + r, cy));
226 ig.LineTo(new Point(cx, cy + r));
227 ig.LineTo(new Point(cx - r, cy));
228 ig.EndFigure(true);
229 }
230
231 context.DrawGeometry(theme.ConditionFill, theme.NodeStrokePen, geo);
232
233 var cap = new FormattedText(
234 "условие",
235 CultureInfo.InvariantCulture,
236 FlowDirection.LeftToRight,
237 theme.SideLabelTypeface,
238 captionSize,
239 theme.SideLabelBrush);
240 context.DrawText(cap, new Point(x0 + r * 2 + 10, y + (rowH - cap.Height) / 2));
241 }
242
243 private static void DrawLegendExceptionFlowKeyRow(DrawingContext context, SkiaGraphVisualTheme theme, double x0, double y, double rowH, double captionSize)
244 {
245 const double iconR = 5.5;
246 var cy = y + rowH / 2;
247 var cx = x0 + iconR + 1;
248 context.DrawEllipse(theme.HandlerFill, theme.NodeStrokePen, new Point(cx, cy), iconR, iconR);
249 var ex = new FormattedText(
250 "!",
251 CultureInfo.InvariantCulture,
252 FlowDirection.LeftToRight,
253 theme.GlyphTypeface,
254 Math.Max(7, captionSize - 1),
255 theme.GlyphBrush);
256 context.DrawText(ex, new Point(cx - ex.Width / 2, cy - ex.Height / 2));
257
258 var cap = new FormattedText(
259 "catch / handler",
260 CultureInfo.InvariantCulture,
261 FlowDirection.LeftToRight,
262 theme.SideLabelTypeface,
263 captionSize,
264 theme.SideLabelBrush);
265 context.DrawText(cap, new Point(x0 + iconR * 2 + 10, y + (rowH - cap.Height) / 2));
266 }
267
268 private static double DrawLegendEdgeStyleKeyBlock(
269 DrawingContext context,
270 SkiaGraphVisualTheme theme,
271 double x0,
272 double y,
273 double keyRowH,
274 double captionSize,
275 double viewportBottom,
276 IReadOnlyList<GraphLayoutEdge> edges)
277 {
278 static bool KindContains(string? kind, string needle) =>
279 !string.IsNullOrEmpty(kind) && kind.Contains(needle, StringComparison.OrdinalIgnoreCase);
280
281 var hasNonSolid = edges.Any(e =>
282 KindContains(e.Kind, "conditional")
283 || KindContains(e.Kind, "exception")
284 || KindContains(e.Kind, "multibranch")
285 || KindContains(e.Kind, "loop"));
286 if (!hasNonSolid)
287 return y;
288
289 const double lineLen = 34d;
290 var lineLeft = x0 + 2;
291 var textX = lineLeft + lineLen + 10;
292
293 void OneRow(Pen pen, string text, ref double yRow)
294 {
295 if (yRow + keyRowH > viewportBottom - 4)
296 return;
297 var cy = yRow + keyRowH / 2;
298 context.DrawLine(
299 pen,
300 new Point(lineLeft, cy),
301 new Point(lineLeft + lineLen, cy));
302 var cap = new FormattedText(
303 text,
304 CultureInfo.InvariantCulture,
305 FlowDirection.LeftToRight,
306 theme.SideLabelTypeface,
307 captionSize,
308 theme.SideLabelBrush);
309 context.DrawText(cap, new Point(textX, yRow + (keyRowH - cap.Height) / 2));
310 yRow += keyRowH + 2;
311 }
312
313 // Совпадает с <see cref="SkiaGraphSceneDrawing.Edges"/>: solid — основной поток.
314 OneRow(theme.BaseEdgePen, "основной поток (вызовы, return, последовательно)", ref y);
315 if (edges.Any(e => KindContains(e.Kind, "conditional") || KindContains(e.Kind, "exception")))
316 OneRow(theme.ConditionalEdgePen, "длинные штрихи — ветвления, catch, исключения", ref y);
317 if (edges.Any(e => KindContains(e.Kind, "multibranch")))
318 OneRow(theme.MultiBranchEdgePen, "короткие штрихи — switch", ref y);
319 if (edges.Any(e => KindContains(e.Kind, "loop")))
320 OneRow(theme.LoopEdgePen, "толстая линия / кольцо — тело цикла", ref y);
321 return y;
322 }
323}
324
View only · write via MCP/CIDE