Forge
csharpdeeb25a2
1#nullable enable
2
3using CascadeIDE.Models.Intercom;
4using CascadeIDE.Services;
5using CascadeIDE.Views.SkiaKit;
6using SkiaSharp;
7
8namespace CascadeIDE.Views.Chat.Skia;
9
10/// <summary>Attach-ссылка в ленте: pill + рамка/иконка статуса resolve + label (ADR 0134).</summary>
11internal static class SkiaIntercomAttachLinkChip
12{
13 private const float DefaultLabelFontSize = 11f;
14
15 public static IntercomAttachLinkVisualStatus Classify(AttachmentAnchor? anchor, bool messagePending)
16 {
17 if (messagePending)
18 return IntercomAttachLinkVisualStatus.Pending;
19
20 if (anchor is null)
21 return IntercomAttachLinkVisualStatus.Failed;
22
23 var outcome = anchor.ResolveOutcome?.Trim();
24 if (string.Equals(outcome, IntercomAttachmentRevealPlan.OutcomeFileMissing, StringComparison.OrdinalIgnoreCase))
25 return IntercomAttachLinkVisualStatus.Failed;
26
27 if (string.IsNullOrWhiteSpace(anchor.File))
28 return IntercomAttachLinkVisualStatus.Pending;
29
30 if (string.Equals(outcome, IntercomAttachmentRevealPlan.OutcomeMemberNotFound, StringComparison.OrdinalIgnoreCase))
31 return IntercomAttachLinkVisualStatus.Degraded;
32
33 return IntercomAttachLinkVisualStatus.Resolved;
34 }
35
36 public static float MeasureHeight(bool forwardHost, float labelFontSize = DefaultLabelFontSize)
37 {
38 var baseH = forwardHost ? SkiaStatusChip.MinHeight - 2f : SkiaStatusChip.MinHeight;
39 return baseH * (labelFontSize / DefaultLabelFontSize);
40 }
41
42 public static float MeasureWidth(
43 string label,
44 string? anchorShortId,
45 float maxContentWidth,
46 float labelFontSize = DefaultLabelFontSize,
47 string? chipFamily = null,
48 string? chipIdFamily = null)
49 {
50 using var font = CreateLabelFont(labelFontSize, chipFamily);
51 using var idFont = CreateIdFont(labelFontSize, chipIdFamily);
52 var text = normalizeLabel(label);
53 var textW = font.MeasureText(text);
54 var idW = string.IsNullOrWhiteSpace(anchorShortId) ? 0f : idFont.MeasureText(formatIdSuffix(anchorShortId)) + 4f;
55 var w = SkiaStatusChip.ComputeContentWidth(textW + idW);
56 return Math.Min(Math.Max(48f, w), Math.Max(80f, maxContentWidth));
57 }
58
59 /// <summary>Ширина chip без усечения под колонку (для inline-раскладки prose+chip).</summary>
60 public static float MeasureIntrinsicWidth(
61 string label,
62 string? anchorShortId,
63 float labelFontSize = DefaultLabelFontSize,
64 string? chipFamily = null,
65 string? chipIdFamily = null)
66 {
67 using var font = CreateLabelFont(labelFontSize, chipFamily);
68 using var idFont = CreateIdFont(labelFontSize, chipIdFamily);
69 var text = normalizeLabel(label);
70 var textW = font.MeasureText(text);
71 var idW = string.IsNullOrWhiteSpace(anchorShortId)
72 ? 0f
73 : idFont.MeasureText(formatIdSuffix(anchorShortId)) + 4f;
74 return Math.Max(48f, SkiaStatusChip.ComputeContentWidth(textW + idW));
75 }
76
77 public static void Draw(
78 SKCanvas canvas,
79 SkiaChatTheme theme,
80 SKRect chipRect,
81 string label,
82 IntercomAttachLinkVisualStatus status,
83 string? anchorShortId = null,
84 float labelFontSize = DefaultLabelFontSize,
85 string? chipFamily = null,
86 string? chipIdFamily = null)
87 {
88 var colors = SkiaStatusChip.ResolveColors(theme, ToSeverity(status), theme.MutedContent);
89 SkiaStatusChip.DrawFrame(canvas, chipRect, colors);
90 SkiaStatusChip.DrawIcon(
91 canvas,
92 SkiaStatusChip.IconCenterInRect(chipRect, SkiaStatusChipIconPlacement.Left),
93 ToSeverity(status),
94 colors.Icon,
95 labelFontSize);
96
97 using var labelFont = CreateLabelFont(labelFontSize, chipFamily);
98 var text = normalizeLabel(label);
99 var textLeft = SkiaStatusChip.ContentLeftInRect(chipRect);
100 var baseline = chipRect.MidY + labelFont.Size * 0.35f;
101 using var labelPaint = new SKPaint { IsAntialias = true, Color = colors.Accent };
102 canvas.DrawText(text, textLeft, baseline, SKTextAlign.Left, labelFont, labelPaint);
103
104 if (!string.IsNullOrWhiteSpace(anchorShortId))
105 {
106 using var idFont = CreateIdFont(labelFontSize, chipIdFamily);
107 var idText = formatIdSuffix(anchorShortId);
108 var idLeft = textLeft + labelFont.MeasureText(text) + 4f;
109 using var idPaint = new SKPaint { IsAntialias = true, Color = theme.MutedContent };
110 canvas.DrawText(idText, idLeft, baseline, SKTextAlign.Left, idFont, idPaint);
111 }
112 }
113
114 public static SKRect ComputeHitRect(SKRect chipRect) =>
115 chipRect;
116
117 private static string normalizeLabel(string label)
118 {
119 var t = label.Trim();
120 if (t.Length >= 2 && t[0] == '[' && t[^1] == ']')
121 return t[1..^1];
122 return t;
123 }
124
125 private static SKFont CreateLabelFont(float labelFontSize, string? chipFamily) =>
126 SkiaChatFeedFontResolver.CreateFont(
127 string.IsNullOrWhiteSpace(chipFamily) ? "Segoe UI" : chipFamily,
128 labelFontSize);
129
130 private static SKFont CreateIdFont(float labelFontSize, string? chipIdFamily) =>
131 SkiaChatFeedFontResolver.CreateFont(
132 string.IsNullOrWhiteSpace(chipIdFamily) ? "Consolas" : chipIdFamily,
133 labelFontSize - 1f);
134
135 private static string formatIdSuffix(string anchorShortId) =>
136 $"a:{anchorShortId.Trim().ToLowerInvariant()}";
137
138 private static SkiaStatusChipSeverity ToSeverity(IntercomAttachLinkVisualStatus status) =>
139 status switch
140 {
141 IntercomAttachLinkVisualStatus.Resolved => SkiaStatusChipSeverity.Success,
142 IntercomAttachLinkVisualStatus.Degraded => SkiaStatusChipSeverity.Warning,
143 IntercomAttachLinkVisualStatus.Pending => SkiaStatusChipSeverity.Pending,
144 _ => SkiaStatusChipSeverity.Error,
145 };
146}
147
148internal enum IntercomAttachLinkVisualStatus
149{
150 Resolved = 0,
151 Degraded = 1,
152 Failed = 2,
153 Pending = 3,
154}
155
View only · write via MCP/CIDE