Forge
csharpdeeb25a2
1#nullable enable
2
3using CascadeIDE.Features.Chat;
4using CascadeIDE.Models;
5using CascadeIDE.Models.Intercom;
6using CascadeIDE.Views.SkiaKit;
7using SkiaSharp;
8
9namespace CascadeIDE.Views.Chat.Skia;
10
11/// <summary>
12/// Результат локальной слэш-команды в ленте — flat feed (ADR 0123).
13/// Геометрия — <see cref="SkiaChatFeedLayout"/>.
14/// </summary>
15internal sealed class SkiaChatSlashCommandEntity(
16 ChatSurfaceEntry entry,
17 bool forwardHost,
18 int feedOrdinal = 0,
19 IntercomFontsSettings? intercomFonts = null) : ISkiaChatEntity
20{
21 private const float GapAfter = 6f;
22
23 private readonly int _feedOrdinal = feedOrdinal;
24 private readonly SkiaChatFeedLayout _layout = SkiaChatFeedLayout.For(forwardHost, intercomFonts);
25 private readonly string _slashPath = entry.SlashCommandPath ?? "";
26 private readonly string? _args = entry.SlashCommandArgs;
27 private readonly string? _detail = string.IsNullOrWhiteSpace(entry.Body) ? null : entry.Body;
28 private readonly ChatSlashCommandStatus _status = entry.SlashCommandStatus ?? ChatSlashCommandStatus.Running;
29 private readonly bool _isLocalSelfOnly = entry.Audience == IntercomMessageAudience.SelfOnly;
30 private readonly string _roleLabel = entry.Title;
31
32 public SkiaChatMeasuredLayout Measure(SkiaChatMeasureContext context)
33 {
34 var column = _layout.SlashTextColumn(0f, context.ContentWidth);
35 SkiaRichTextKitBodyLayout? richDetail = null;
36 var detailHeight = 0f;
37 if (!string.IsNullOrWhiteSpace(_detail))
38 {
39 richDetail = SkiaRichTextKitMarkdown.TryMeasureDocument(
40 _detail,
41 column.Width,
42 baseFontSize: _layout.ProseFontSize,
43 contentColor: new SKColor(220, 225, 235),
44 codeColor: new SKColor(180, 190, 210),
45 maxRows: SkiaChatRenderLimits.MaxDocumentRows,
46 lineHeight: _layout.ProseLineHeight,
47 forwardHost,
48 _layout.ProseFamily,
49 _layout.MonoFamily);
50 detailHeight = richDetail?.BodyHeight ?? 0f;
51 }
52
53 var height = _layout.SlashRowHeight(
54 !string.IsNullOrWhiteSpace(_args),
55 !string.IsNullOrWhiteSpace(_detail),
56 detailHeight);
57
58 return new SkiaChatMeasuredLayout(Math.Max(20f, height), GapAfter, RichTextDetail: richDetail);
59 }
60
61 public void Draw(SkiaChatDrawContext context, float top, in SkiaChatMeasuredLayout layout)
62 {
63 var rowBottom = top + layout.Height;
64 var isSelected = context.IsMessageHighlighted(entry.MessageIndex);
65 if (isSelected)
66 DrawRowSelection(context, top, rowBottom);
67
68 if (_feedOrdinal > 0)
69 SkiaChatFeedGutter.DrawOrdinal(context, top, rowBottom, _feedOrdinal, _layout, isSelected);
70
71 var row = _layout.RowRect(context.ContentLeft, context.ContentWidth, top, layout.Height);
72 var column = _layout.SlashTextColumn(context.ContentLeft, context.ContentWidth);
73
74 SkiaChatFeedRoleRail.Draw(
75 context.Canvas,
76 context.Theme,
77 row.Left,
78 row.Top,
79 row.Bottom,
80 _roleLabel,
81 _layout);
82
83 DrawAccentIfNeeded(context, row, _layout.RoleRailWidth);
84
85 var metaBaseline = _layout.SlashMetaBaselineY(row.Top);
86 DrawMetaLine(context, column, metaBaseline);
87
88 var iconRect = SkiaSlashCommandStatusIconRenderer.ResolveIconRect(
89 new SKRect(column.Right, row.Top, row.Right, row.Top + _layout.SlashMetaLineHeight + _layout.SlashAccentGap),
90 ChatSlashCommandPresentation.DefaultStatusIconPlacement);
91 SkiaSlashCommandStatusIconRenderer.Draw(context.Canvas, iconRect, context.Theme, _status);
92
93 if (!string.IsNullOrWhiteSpace(_args))
94 {
95 var argsBaseline = _layout.SlashArgsBandTop(row.Top) + _layout.SlashArgsFontSize * SkiaChatFeedLayout.TextBaselineFactor;
96 using var argsFont = SkiaChatFeedFontResolver.CreateFont(_layout.SlashArgsFamily, _layout.SlashArgsFontSize);
97 using var argsPaint = new SKPaint { IsAntialias = true, Color = context.Theme.MutedContent };
98 context.Canvas.DrawText(_args, column.Left, argsBaseline, SKTextAlign.Left, argsFont, argsPaint);
99 }
100
101 if (!string.IsNullOrWhiteSpace(_detail))
102 {
103 var bodyColor = _status == ChatSlashCommandStatus.Failed
104 ? new SKColor(240, 160, 160)
105 : context.Theme.Content;
106 var codeColor = SkiaKitColor.Blend(context.Theme.Content, context.Theme.HoverBorder, 0.35f);
107 var detailBaseline = _layout.SlashDetailBaselineY(row.Top, !string.IsNullOrWhiteSpace(_args));
108 if (layout.RichTextDetail is { } rich)
109 {
110 var origin = _layout.RichTextPaintOrigin(column.Left, detailBaseline, _layout.ProseFontSize);
111 SkiaRichTextKitMarkdown.Paint(context.Canvas, origin, rich, bodyColor, codeColor);
112 }
113 else
114 {
115 var maxChars = _layout.MaxCharsForWidth(column.Width);
116 var detailRows = SkiaMarkdownDocument.Layout(_detail, maxChars);
117 SkiaMarkdownPainter.Draw(
118 context,
119 column.Left,
120 column.Right,
121 detailBaseline,
122 detailRows,
123 forwardHost,
124 bodyColor,
125 _layout);
126 }
127 }
128
129 if (entry.MessageIndex is { } messageIndex)
130 {
131 var rowRect = new SKRect(
132 _feedOrdinal > 0 ? context.FeedGutterLeft : context.ContentLeft,
133 top,
134 context.ContentLeft + context.ContentWidth,
135 rowBottom);
136 context.RegisterHit(rowRect, new SkiaChatHit(messageIndex, null, ResetDetailMode: false));
137 }
138 }
139
140 public SkiaChatHit? CreateHit(in SkiaChatMeasuredLayout layout) => null;
141
142 private static void DrawRowSelection(SkiaChatDrawContext context, float top, float bottom)
143 {
144 var left = context.FeedGutterLeft;
145 var rect = new SKRect(left, top, context.ContentLeft + context.ContentWidth, bottom);
146 using var rowFill = new SKPaint
147 {
148 Color = SkiaKit.SkiaKitColor.Blend(context.Theme.Surface, context.Theme.SelectedBorder, 0.26f),
149 IsAntialias = true,
150 Style = SKPaintStyle.Fill,
151 };
152 context.Canvas.DrawRect(rect, rowFill);
153 }
154
155 private void DrawAccentIfNeeded(SkiaChatDrawContext context, SKRect row, float roleW)
156 {
157 var accentLeft = row.Left + roleW;
158 if (_status == ChatSlashCommandStatus.Failed)
159 {
160 var bar = new SKRect(
161 accentLeft,
162 row.Top + _layout.RowEdgePad,
163 accentLeft + _layout.SlashAccentWidth,
164 row.Bottom - _layout.RowEdgePad);
165 using var paint = new SKPaint { Color = new SKColor(220, 90, 90, 220), IsAntialias = true, Style = SKPaintStyle.Fill };
166 context.Canvas.DrawRoundRect(bar, 1.5f, 1.5f, paint);
167 return;
168 }
169
170 if (!_isLocalSelfOnly)
171 return;
172
173 var selfBar = new SKRect(
174 accentLeft,
175 row.Top + _layout.RowEdgePad,
176 accentLeft + _layout.SlashAccentWidth,
177 row.Bottom - _layout.RowEdgePad);
178 using var selfPaint = new SKPaint { Color = new SKColor(100, 150, 200, 160), IsAntialias = true, Style = SKPaintStyle.Fill };
179 context.Canvas.DrawRoundRect(selfBar, 1.5f, 1.5f, selfPaint);
180 }
181
182 private void DrawMetaLine(SkiaChatDrawContext context, in SlashTextColumn column, float baselineY)
183 {
184 using var metaFont = SkiaChatFeedFontResolver.CreateFont(
185 _layout.SlashMetaFamily,
186 _layout.SlashMetaFontSize,
187 SKFontStyle.Bold);
188 using var metaPaint = new SKPaint { IsAntialias = true, Color = context.Theme.Role };
189 context.Canvas.DrawText(_slashPath, column.Left, baselineY, SKTextAlign.Left, metaFont, metaPaint);
190 }
191}
192
View only · write via MCP/CIDE