Forge
csharpdeeb25a2
1#nullable enable
2
3using SkiaSharp;
4
5namespace CascadeIDE.Views.SkiaKit;
6
7/// <summary>Семантика pill-рамки (CodeAnchors, slash TCI, attach-chip, …).</summary>
8internal enum SkiaStatusChipSeverity
9{
10 None = 0,
11 Success = 1,
12 Warning = 2,
13 Error = 3,
14 Pending = 4,
15 Info = 5,
16}
17
18/// <summary>Палитра pill: рамка, заливка, иконка, акцентный текст снаружи/внутри.</summary>
19internal readonly record struct SkiaStatusChipColors(
20 SKColor Border,
21 SKColor Fill,
22 SKColor Icon,
23 SKColor Accent);
24
25/// <summary>
26/// Примитив Skia: скруглённая рамка + опциональная иконка (как attach-chip / slash TCI).
27/// Текст рисует вызывающий код с акцентом из палитры.
28/// </summary>
29internal static class SkiaStatusChip
30{
31 public const float PadX = 8f;
32 public const float PadY = 4f;
33 public const float IconBox = 14f;
34 public const float IconGap = 5f;
35 public const float Corner = 6f;
36 public const float MinHeight = 22f;
37 public const float BorderStroke = 1.25f;
38
39 /// <summary>Ширина зоны иконки слева от начала текста (legacy left placement).</summary>
40 public const float IconLeadingOverhang = PadX + IconBox + IconGap;
41
42 /// <summary>Доп. отступ слева в layout при <see cref="SkiaStatusChipIconPlacement.Left"/>.</summary>
43 public const float LeadingChipLayoutGutter = IconLeadingOverhang + BorderStroke + 2f;
44
45 public static float ContentWidthPadding(SkiaStatusChipIconPlacement iconPlacement) =>
46 PadX * 2f + IconZoneWidth(iconPlacement);
47
48 public static float ComputeContentWidth(float labelWidth, SkiaStatusChipIconPlacement iconPlacement) =>
49 ContentWidthPadding(iconPlacement) + labelWidth;
50
51 /// <summary>Pill вокруг slash-текста, начинающегося в <paramref name="textLeft"/>.</summary>
52 public static SKRect ComputeRectAroundTextStart(
53 float textLeft,
54 float textTop,
55 float lineHeight,
56 float labelWidth,
57 SkiaStatusChipIconPlacement iconPlacement = SkiaStatusChipIconPlacement.Right)
58 {
59 var chipW = ComputeContentWidth(labelWidth, iconPlacement);
60 var chipH = Math.Max(lineHeight + PadY * 2f, MinHeight);
61 var chipTop = textTop + (lineHeight - chipH) * 0.5f;
62 var chipLeft = iconPlacement switch
63 {
64 SkiaStatusChipIconPlacement.Left => textLeft - PadX - IconBox - IconGap,
65 _ => textLeft - PadX,
66 };
67 return new SKRect(chipLeft, chipTop, chipLeft + chipW, chipTop + chipH);
68 }
69
70 public static float ContentLeftInRect(
71 SKRect chipRect,
72 SkiaStatusChipIconPlacement iconPlacement = SkiaStatusChipIconPlacement.Left) =>
73 iconPlacement switch
74 {
75 SkiaStatusChipIconPlacement.Right => chipRect.Left + PadX,
76 SkiaStatusChipIconPlacement.HighlightOnly => chipRect.Left + PadX,
77 _ => chipRect.Left + PadX + IconBox + IconGap,
78 };
79
80 public static SKPoint IconCenterInRect(
81 SKRect chipRect,
82 SkiaStatusChipIconPlacement iconPlacement = SkiaStatusChipIconPlacement.Left) =>
83 iconPlacement switch
84 {
85 SkiaStatusChipIconPlacement.Left => new(chipRect.Left + PadX + IconBox * 0.5f, chipRect.MidY),
86 SkiaStatusChipIconPlacement.Right => new(chipRect.Right - PadX - IconBox * 0.5f, chipRect.MidY),
87 _ => new(chipRect.MidX, chipRect.MidY),
88 };
89
90 public static float ComputeContentWidth(float labelWidth) =>
91 ComputeContentWidth(labelWidth, SkiaStatusChipIconPlacement.Left);
92
93 public static SKRect ExpandClipForChip(SKRect clipRect, SKRect chipRect)
94 {
95 var union = SKRect.Union(clipRect, chipRect);
96 union.Inflate(BorderStroke + 1f, BorderStroke + 1f);
97 return union;
98 }
99
100 public static void DrawFrame(SKCanvas canvas, SKRect chipRect, in SkiaStatusChipColors colors)
101 {
102 using var fillPaint = new SKPaint { Color = colors.Fill, IsAntialias = true, Style = SKPaintStyle.Fill };
103 canvas.DrawRoundRect(chipRect, Corner, Corner, fillPaint);
104 using var borderPaint = new SKPaint
105 {
106 Color = colors.Border,
107 IsAntialias = true,
108 Style = SKPaintStyle.Stroke,
109 StrokeWidth = BorderStroke,
110 };
111 canvas.DrawRoundRect(chipRect, Corner, Corner, borderPaint);
112 }
113
114 public static void DrawIcon(
115 SKCanvas canvas,
116 SKPoint center,
117 SkiaStatusChipSeverity severity,
118 SKColor color,
119 float fontSize)
120 {
121 var glyph = GlyphFor(severity);
122 if (glyph.Length == 0)
123 return;
124
125 DrawIconGlyph(canvas, center, glyph, color, fontSize);
126 }
127
128 public static void DrawIconGlyph(
129 SKCanvas canvas,
130 SKPoint center,
131 string glyph,
132 SKColor color,
133 float fontSize)
134 {
135 if (string.IsNullOrEmpty(glyph))
136 return;
137
138 using var font = new SKFont(
139 SKTypeface.FromFamilyName("Segoe UI Symbol", SKFontStyle.Normal),
140 fontSize);
141 using var paint = new SKPaint { IsAntialias = true, Color = color };
142 canvas.DrawText(glyph, center.X, center.Y + 4f, SKTextAlign.Center, font, paint);
143 }
144
145 public static string GlyphFor(SkiaStatusChipSeverity severity) =>
146 severity switch
147 {
148 SkiaStatusChipSeverity.Success => "\u2713",
149 SkiaStatusChipSeverity.Warning => "\u26A0",
150 SkiaStatusChipSeverity.Error => "\u2715",
151 SkiaStatusChipSeverity.Pending => "\u23F1",
152 SkiaStatusChipSeverity.Info => "\u2139",
153 _ => "",
154 };
155
156 public static void DrawChrome(
157 SKCanvas canvas,
158 SKRect chipRect,
159 ISkiaKitPaintTheme theme,
160 SkiaStatusChipSeverity severity,
161 float iconFontSize,
162 SkiaStatusChipIconPlacement iconPlacement = SkiaStatusChipIconPlacement.Right)
163 {
164 if (severity == SkiaStatusChipSeverity.None)
165 return;
166
167 var colors = ResolveColors(theme, severity);
168 DrawFrame(canvas, chipRect, colors);
169 if (iconPlacement != SkiaStatusChipIconPlacement.HighlightOnly)
170 DrawIcon(canvas, IconCenterInRect(chipRect, iconPlacement), severity, colors.Icon, iconFontSize);
171 }
172
173 public static SkiaStatusChipColors ResolveColors(
174 ISkiaKitPaintTheme theme,
175 SkiaStatusChipSeverity severity,
176 SKColor? mutedContent = null) =>
177 ResolveColors(theme, severity, mutedContent ?? theme.EmptyHint);
178
179 private static float IconZoneWidth(SkiaStatusChipIconPlacement iconPlacement) =>
180 iconPlacement == SkiaStatusChipIconPlacement.HighlightOnly ? 0f : IconBox + IconGap;
181
182 private static SkiaStatusChipColors ResolveColors(
183 ISkiaKitPaintTheme theme,
184 SkiaStatusChipSeverity severity,
185 SKColor mutedContent) =>
186 severity switch
187 {
188 SkiaStatusChipSeverity.Success => new(
189 new SKColor(72, 160, 110, 200),
190 SkiaKitColor.Blend(theme.Surface, new SKColor(72, 160, 110), 0.12f),
191 new SKColor(100, 200, 140),
192 new SKColor(120, 200, 255)),
193 SkiaStatusChipSeverity.Warning => new(
194 new SKColor(200, 150, 70, 210),
195 SkiaKitColor.Blend(theme.Surface, new SKColor(200, 150, 70), 0.14f),
196 new SKColor(230, 180, 90),
197 new SKColor(140, 190, 255)),
198 SkiaStatusChipSeverity.Error => new(
199 new SKColor(190, 90, 90, 200),
200 SkiaKitColor.Blend(theme.Surface, new SKColor(190, 90, 90), 0.12f),
201 new SKColor(220, 110, 110),
202 SkiaKitColor.Blend(theme.EmptyHint, theme.Content, 0.35f)),
203 SkiaStatusChipSeverity.Pending => new(
204 SkiaKitColor.Blend(theme.Border, mutedContent, 0.5f),
205 SkiaKitColor.Blend(theme.Surface, theme.Border, 0.2f),
206 mutedContent,
207 SkiaKitColor.Blend(theme.Content, theme.HoverBorder, 0.45f)),
208 SkiaStatusChipSeverity.Info => new(
209 SkiaKitColor.Blend(theme.Border, theme.EmptyHint, 0.5f),
210 SkiaKitColor.Blend(theme.Surface, theme.Border, 0.2f),
211 theme.EmptyHint,
212 SkiaKitColor.Blend(theme.Content, theme.HoverBorder, 0.45f)),
213 _ => new(theme.Border, theme.Surface, theme.EmptyHint, theme.Content),
214 };
215}
216
View only · write via MCP/CIDE