Forge
csharpdeeb25a2
1#nullable enable
2using CascadeIDE.Views.SkiaKit;
3using SkiaSharp;
4
5namespace CascadeIDE.Views.Chat.Skia;
6
7internal static class SkiaMarkdownPainter
8{
9 public static float MeasureHeight(IReadOnlyList<SkiaMarkdownRow> rows, bool compact) =>
10 rows.Count == 0 ? 0f : rows.Sum(r => RowHeight(r.Kind, compact));
11
12 public static float Draw(
13 SkiaChatDrawContext context,
14 float left,
15 float right,
16 float top,
17 IReadOnlyList<SkiaMarkdownRow> rows,
18 bool compact,
19 SKColor bodyColor) =>
20 Draw(context, left, right, top, rows, compact, bodyColor, SkiaChatFeedLayout.For(compact));
21
22 public static float Draw(
23 SkiaChatDrawContext context,
24 float left,
25 float right,
26 float top,
27 IReadOnlyList<SkiaMarkdownRow> rows,
28 bool compact,
29 SKColor bodyColor,
30 in SkiaChatFeedLayout layout)
31 {
32 var y = top;
33 foreach (var row in rows)
34 {
35 y += DrawRow(context, left, right, y, row, compact, bodyColor, layout);
36 }
37
38 return y;
39 }
40
41 private static float DrawRow(
42 SkiaChatDrawContext context,
43 float left,
44 float right,
45 float baselineY,
46 SkiaMarkdownRow row,
47 bool compact,
48 SKColor bodyColor,
49 in SkiaChatFeedLayout layout)
50 {
51 var height = RowHeight(row.Kind, compact);
52 if (row.Kind == SkiaMarkdownBlockKind.HorizontalRule)
53 {
54 var midY = baselineY + height * 0.45f;
55 using var paint = new SKPaint
56 {
57 Color = SkiaKitColor.Blend(context.Theme.Border, context.Theme.Content, 0.35f),
58 IsAntialias = true,
59 Style = SKPaintStyle.Stroke,
60 StrokeWidth = 1,
61 };
62 context.Canvas.DrawLine(left, midY, right, midY, paint);
63 return height;
64 }
65
66 if (row.Kind == SkiaMarkdownBlockKind.Blank || row.Runs.Count == 0)
67 return height;
68
69 var (bodySize, boldSize) = BodySizes(row.Kind, compact);
70 using var bodyFont = SkiaChatFeedFontResolver.CreateFont(layout.ProseFamily, bodySize);
71 var textY = baselineY + BaselineOffset(row.Kind, compact);
72
73 var x = left;
74 foreach (var run in row.Runs)
75 {
76 if (run.Text.Length == 0)
77 continue;
78
79 var (font, color, disposeFont) = ResolveRunStyle(
80 context,
81 layout,
82 bodyFont,
83 bodySize,
84 boldSize,
85 run.Style,
86 row.Kind,
87 bodyColor);
88 try
89 {
90 using var paint = new SKPaint { IsAntialias = true, Color = color };
91 context.Canvas.DrawText(run.Text, x, textY, SKTextAlign.Left, font, paint);
92 x += font.MeasureText(run.Text);
93 }
94 finally
95 {
96 if (disposeFont)
97 font.Dispose();
98 }
99 }
100
101 return height;
102 }
103
104 private static (SKFont Font, SKColor Color, bool DisposeFont) ResolveRunStyle(
105 SkiaChatDrawContext ctx,
106 in SkiaChatFeedLayout layout,
107 SKFont bodyFont,
108 float bodySize,
109 float boldSize,
110 SkiaMarkdownStyle style,
111 SkiaMarkdownBlockKind block,
112 SKColor bodyColor)
113 {
114 var heading = block is SkiaMarkdownBlockKind.Heading1
115 or SkiaMarkdownBlockKind.Heading2
116 or SkiaMarkdownBlockKind.Heading3;
117
118 if (heading && style is SkiaMarkdownStyle.Plain or SkiaMarkdownStyle.Bold)
119 return (
120 SkiaChatFeedFontResolver.CreateFont(layout.ProseFamily, boldSize, SKFontStyle.Bold),
121 bodyColor,
122 true);
123
124 return style switch
125 {
126 SkiaMarkdownStyle.Bold => (
127 SkiaChatFeedFontResolver.CreateFont(layout.ProseFamily, bodySize, SKFontStyle.Bold),
128 bodyColor,
129 true),
130 SkiaMarkdownStyle.Italic => (
131 SkiaChatFeedFontResolver.CreateFont(layout.ProseFamily, bodySize, SKFontStyle.Italic),
132 bodyColor,
133 true),
134 SkiaMarkdownStyle.Code => (
135 SkiaChatFeedFontResolver.CreateFont(layout.MonoFamily, bodySize * 0.95f),
136 SkiaKitColor.Blend(bodyColor, ctx.Theme.HoverBorder, 0.35f),
137 true),
138 _ => (bodyFont, bodyColor, false),
139 };
140 }
141
142 private static float RowHeight(SkiaMarkdownBlockKind kind, bool compact) =>
143 kind switch
144 {
145 SkiaMarkdownBlockKind.Heading1 => compact ? 20f : 22f,
146 SkiaMarkdownBlockKind.Heading2 => compact ? 17f : 19f,
147 SkiaMarkdownBlockKind.Heading3 => compact ? 15.5f : 17f,
148 SkiaMarkdownBlockKind.HorizontalRule => compact ? 10f : 12f,
149 SkiaMarkdownBlockKind.Blank => compact ? 5f : 6f,
150 _ => compact ? 14f : 15f,
151 };
152
153 private static float BaselineOffset(SkiaMarkdownBlockKind kind, bool compact) =>
154 kind switch
155 {
156 SkiaMarkdownBlockKind.Heading1 => compact ? 14f : 16f,
157 SkiaMarkdownBlockKind.Heading2 => compact ? 12.5f : 14f,
158 SkiaMarkdownBlockKind.Heading3 => compact ? 11.5f : 13f,
159 _ => compact ? 11f : 12f,
160 };
161
162 private static (float BodySize, float BoldSize) BodySizes(SkiaMarkdownBlockKind kind, bool compact) =>
163 kind switch
164 {
165 SkiaMarkdownBlockKind.Heading1 => compact ? (12f, 13f) : (13f, 14f),
166 SkiaMarkdownBlockKind.Heading2 => compact ? (11.5f, 12f) : (12f, 12.5f),
167 SkiaMarkdownBlockKind.Heading3 => compact ? (11f, 11.5f) : (11.5f, 12f),
168 _ => compact ? (11f, 11f) : (11.5f, 11.5f),
169 };
170}
171
View only · write via MCP/CIDE