Forge
csharpdeeb25a2
1#nullable enable
2using CascadeIDE.Views.Chat.Skia;
3using SkiaSharp;
4using Topten.RichTextKit;
5
6namespace CascadeIDE.Views.SkiaKit;
7
8/// <summary>
9/// Общий Text-Command Interface (TCI): caret, hit-test, selection, horizontal scroll.
10/// Используется composer, CCL, navigator search (ADR 0123, 0138).
11/// </summary>
12internal static class SkiaTciTextField
13{
14 public static readonly SKColor CaretColor = new(120, 195, 255);
15 public static readonly SKColor SelectionFill = new(70, 130, 220, 90);
16 private const float CaretStrokeWidth = 2f;
17
18 public readonly record struct Typography(float FontSize, float LineHeight, string FontFamily);
19
20 public readonly record struct Region(SKRect TextBounds, float ContentWidth, float TextTopInset);
21
22 public static string MergeDisplay(string? text, string? preedit) =>
23 string.IsNullOrEmpty(preedit) ? text ?? "" : (text ?? "") + preedit;
24
25 public static float MeasureSingleLineWidth(string? text, Typography typo)
26 {
27 if (string.IsNullOrEmpty(text))
28 return 0f;
29
30 return typo.FontFamily.Contains("Mono", StringComparison.OrdinalIgnoreCase)
31 || typo.FontFamily.Contains("Cascadia", StringComparison.OrdinalIgnoreCase)
32 || typo.FontFamily.Contains("Consolas", StringComparison.OrdinalIgnoreCase)
33 ? SkiaKitFonts.CreateMono(typo.FontSize).MeasureText(text)
34 : SkiaPlainTextLayout.MeasurePrefixWidth(text, typo.FontSize, typo.FontFamily);
35 }
36
37 public static float MaxHorizontalScroll(string? display, float contentWidth, Typography typo) =>
38 Math.Max(0f, MeasureSingleLineWidth(display, typo) - contentWidth);
39
40 public static bool TryHitTestCaret(
41 Region region,
42 string? display,
43 Typography typo,
44 float pointX,
45 float pointY,
46 float scrollOffsetX,
47 float scrollOffsetY,
48 int maxLines,
49 out int caretIndex)
50 {
51 caretIndex = 0;
52 if (!region.TextBounds.Contains(pointX, pointY))
53 return false;
54
55 var localX = pointX - region.TextBounds.Left + scrollOffsetX;
56 var localY = pointY - region.TextBounds.Top - region.TextTopInset + scrollOffsetY;
57 return SkiaPlainTextLayout.TryHitTestCaretIndex(
58 display,
59 localX,
60 localY,
61 region.ContentWidth,
62 typo.FontSize,
63 typo.LineHeight,
64 out caretIndex,
65 maxLines,
66 typo.FontFamily);
67 }
68
69 public static bool TryGetCaretRect(
70 Region region,
71 string? display,
72 int caretIndex,
73 Typography typo,
74 SKPoint textOrigin,
75 float scrollOffsetX,
76 float scrollOffsetY,
77 out SKRect caretRect)
78 {
79 caretRect = default;
80 var origin = new SKPoint(textOrigin.X - scrollOffsetX, textOrigin.Y - scrollOffsetY);
81 if (!SkiaPlainTextLayout.TryGetCaretLine(
82 display,
83 caretIndex,
84 region.ContentWidth,
85 typo.FontSize,
86 typo.LineHeight,
87 origin,
88 out var x,
89 out var yTop,
90 out var yBottom,
91 int.MaxValue,
92 typo.FontFamily))
93 return false;
94
95 caretRect = new SKRect(x, yTop, x + CaretStrokeWidth, yBottom);
96 return true;
97 }
98
99 public static void DrawSelection(
100 SKCanvas canvas,
101 string display,
102 int selectionAnchor,
103 int caretIndex,
104 Region region,
105 Typography typo,
106 SKPoint textOrigin,
107 float scrollOffsetX,
108 float scrollOffsetY,
109 int maxLines)
110 {
111 var anchor = selectionAnchor;
112 var start = Math.Min(anchor, caretIndex);
113 var end = Math.Max(anchor, caretIndex);
114 if (start >= end)
115 return;
116
117 var rs = SkiaPlainTextLayout.BuildRichString(
118 display,
119 region.ContentWidth,
120 typo.FontSize,
121 typo.LineHeight,
122 maxLines,
123 fontFamily: typo.FontFamily);
124
125 var infoStart = rs.GetCaretInfo(new CaretPosition(start, altPosition: false));
126 var infoEnd = rs.GetCaretInfo(new CaretPosition(end, altPosition: false));
127 if (infoStart.IsNone || infoEnd.IsNone)
128 return;
129
130 using var paint = new SKPaint { Color = SelectionFill, IsAntialias = true, Style = SKPaintStyle.Fill };
131 var line0 = Math.Min(infoStart.LineIndex, infoEnd.LineIndex);
132 var line1 = Math.Max(infoStart.LineIndex, infoEnd.LineIndex);
133 for (var line = line0; line <= line1; line++)
134 {
135 float left;
136 float right;
137 if (line == line0 && line == line1)
138 {
139 left = textOrigin.X + infoStart.CaretXCoord - scrollOffsetX;
140 right = textOrigin.X + infoEnd.CaretXCoord - scrollOffsetX;
141 }
142 else if (line == line0)
143 {
144 left = textOrigin.X + infoStart.CaretXCoord - scrollOffsetX;
145 right = textOrigin.X + region.ContentWidth - scrollOffsetX;
146 }
147 else if (line == line1)
148 {
149 left = textOrigin.X - scrollOffsetX;
150 right = textOrigin.X + infoEnd.CaretXCoord - scrollOffsetX;
151 }
152 else
153 {
154 left = textOrigin.X - scrollOffsetX;
155 right = textOrigin.X + region.ContentWidth - scrollOffsetX;
156 }
157
158 if (right < left)
159 (left, right) = (right, left);
160
161 var top = textOrigin.Y + line * typo.LineHeight - scrollOffsetY + 2f;
162 var bottom = top + typo.LineHeight - 4f;
163 canvas.DrawRect(SKRect.Create(left, top, right - left, bottom - top), paint);
164 }
165 }
166
167 public static void DrawCaretLine(SKCanvas canvas, float x, float yTop, float yBottom)
168 {
169 using var caret = new SKPaint
170 {
171 Color = CaretColor,
172 StrokeWidth = CaretStrokeWidth,
173 IsAntialias = true,
174 StrokeCap = SKStrokeCap.Round,
175 };
176 canvas.DrawLine(x, yTop, x, yBottom, caret);
177 }
178
179 public static void PaintBody(
180 SKCanvas canvas,
181 SKPoint origin,
182 SkiaRichTextKitBodyLayout layout,
183 SKColor contentColor,
184 SKColor codeColor) =>
185 SkiaRichTextKitMarkdown.Paint(canvas, origin, layout, contentColor, codeColor);
186}
187
View only · write via MCP/CIDE