Forge
csharpdeeb25a2
1#nullable enable
2using SkiaSharp;
3
4namespace CascadeIDE.Views.SkiaKit;
5
6/// <summary>
7/// Карточка с отсеками (UML Class–style compartments): подпись секции, разделитель, строки.
8/// Для чата, graph-backed surfaces и прочих IDE-Skia поверхностей.
9/// </summary>
10internal static class SkiaSectionedCard
11{
12 public const float CornerRadius = 12f;
13 public const float CompartmentLabelHeight = 18f;
14 public const float CompartmentPaddingY = 6f;
15 public const float LineHeight = 16f;
16 public const float HorizontalPadding = 12f;
17 public const float MinHeight = 132f;
18
19 public static SkiaSectionedCardModel FromThreeCompartments(
20 string topicLabel,
21 IReadOnlyList<string> topicLines,
22 string tagsLabel,
23 IReadOnlyList<string> tagLines,
24 string summaryLabel,
25 IReadOnlyList<string> summaryLines) =>
26 new([
27 new SkiaSectionedCardSection(topicLabel, topicLines, SkiaSectionedSectionStyle.Header),
28 new SkiaSectionedCardSection(tagsLabel, tagLines),
29 new SkiaSectionedCardSection(summaryLabel, summaryLines)
30 ]);
31
32 public static float MeasureCompartmentHeight(int lineCount) =>
33 CompartmentLabelHeight
34 + CompartmentPaddingY
35 + Math.Max(1, lineCount) * LineHeight
36 + CompartmentPaddingY;
37
38 public static float MeasureTotalHeight(in SkiaSectionedCardModel model)
39 {
40 var inner = model.Sections.Sum(s => MeasureCompartmentHeight(Math.Max(1, s.Lines.Count)));
41 return Math.Max(MinHeight, HorizontalPadding * 2 + inner);
42 }
43
44 public static void Draw(
45 SKCanvas canvas,
46 ISkiaKitPaintTheme theme,
47 SKRect bounds,
48 float contentLeft,
49 float contentWidth,
50 in SkiaSectionedCardModel model,
51 in SkiaSectionedCardDrawState state)
52 {
53 DrawShadow(canvas, bounds, CornerRadius);
54 using (var fill = new SKPaint
55 {
56 Color = state.FillColor,
57 IsAntialias = true,
58 Style = SKPaintStyle.Fill
59 })
60 canvas.DrawRoundRect(bounds, CornerRadius, CornerRadius, fill);
61
62 DrawFrame(canvas, theme, bounds, in state);
63
64 var y = bounds.Top + HorizontalPadding;
65 foreach (var section in model.Sections)
66 {
67 y = DrawCompartment(
68 canvas,
69 theme,
70 contentLeft,
71 bounds.Right,
72 y,
73 section.Label,
74 section.Lines,
75 section.Style == SkiaSectionedSectionStyle.Header);
76 }
77 }
78
79 private static void DrawShadow(SKCanvas canvas, SKRect bounds, float corner)
80 {
81 var shadowRect = bounds;
82 shadowRect.Offset(0, 4);
83 using var shadowPaint = new SKPaint
84 {
85 Color = new SKColor(0, 0, 0, 88),
86 IsAntialias = true,
87 Style = SKPaintStyle.Fill
88 };
89 canvas.DrawRoundRect(shadowRect, corner + 1, corner + 1, shadowPaint);
90 }
91
92 private static void DrawFrame(SKCanvas canvas, ISkiaKitPaintTheme theme, SKRect bounds, in SkiaSectionedCardDrawState state)
93 {
94 var borderColor = state.IsFocused
95 ? theme.SelectedBorder
96 : state.IsHovered
97 ? theme.HoverBorder
98 : SkiaKitColor.Blend(theme.Border, theme.Content, 0.62f);
99 using var stroke = new SKPaint
100 {
101 Color = borderColor,
102 IsAntialias = true,
103 Style = SKPaintStyle.Stroke,
104 StrokeWidth = state.IsFocused ? 2.4f : state.IsHovered ? 2f : 1.6f
105 };
106 canvas.DrawRoundRect(bounds, CornerRadius, CornerRadius, stroke);
107 }
108
109 private static float DrawCompartment(
110 SKCanvas canvas,
111 ISkiaKitPaintTheme theme,
112 float contentLeft,
113 float boundsRight,
114 float top,
115 string label,
116 IReadOnlyList<string> lines,
117 bool isHeader)
118 {
119 var compartmentTop = top;
120 var labelBaseline = compartmentTop + CompartmentLabelHeight - 5f;
121
122 using var labelFont = new SKFont(SKTypeface.FromFamilyName("Segoe UI", SKFontStyle.Bold), 8.5f);
123 using var labelPaint = new SKPaint
124 {
125 IsAntialias = true,
126 Color = SkiaKitColor.Blend(theme.Role, theme.EmptyHint, 0.22f)
127 };
128 canvas.DrawText(label, contentLeft, labelBaseline, SKTextAlign.Left, labelFont, labelPaint);
129
130 var sepY = compartmentTop + CompartmentLabelHeight;
131 using var sepPaint = new SKPaint
132 {
133 Color = SkiaKitColor.Blend(theme.Border, theme.Content, 0.48f),
134 IsAntialias = true,
135 Style = SKPaintStyle.Stroke,
136 StrokeWidth = 1.2f
137 };
138 canvas.DrawLine(contentLeft, sepY, boundsRight - HorizontalPadding, sepY, sepPaint);
139
140 if (isHeader)
141 {
142 using var titleFont = new SKFont(SKTypeface.FromFamilyName("Segoe UI", SKFontStyle.Bold), 13f);
143 using var titlePaint = new SKPaint { IsAntialias = true, Color = theme.Content };
144 var titleY = sepY + CompartmentPaddingY + 12f;
145 foreach (var line in lines)
146 {
147 canvas.DrawText(line, contentLeft, titleY, SKTextAlign.Left, titleFont, titlePaint);
148 titleY += LineHeight;
149 }
150
151 return compartmentTop + MeasureCompartmentHeight(lines.Count);
152 }
153
154 using var bodyFont = new SKFont(SKTypeface.FromFamilyName("Segoe UI"), 11f);
155 var textY = sepY + CompartmentPaddingY + 12f;
156 foreach (var line in lines)
157 {
158 var isPlaceholder = IsPlaceholderLine(line);
159 using var linePaint = new SKPaint
160 {
161 IsAntialias = true,
162 Color = isPlaceholder
163 ? theme.EmptyHint
164 : SkiaKitColor.Blend(theme.Content, theme.EmptyHint, 0.18f)
165 };
166 canvas.DrawText(line, contentLeft, textY, SKTextAlign.Left, bodyFont, linePaint);
167 textY += LineHeight;
168 }
169
170 return compartmentTop + MeasureCompartmentHeight(lines.Count);
171 }
172
173 internal static bool IsPlaceholderLine(string line) =>
174 line is "—" or "нет тегов"
175 || line.StartsWith("Нет краткого", StringComparison.Ordinal)
176 || line.StartsWith("Пока без сообщений", StringComparison.Ordinal)
177 || line.StartsWith("Задай фокус", StringComparison.Ordinal);
178}
179
View only · write via MCP/CIDE