Forge
csharpdeeb25a2
1#nullable enable
2using CascadeIDE.Views.Chat.Skia;
3using SkiaSharp;
4using Topten.RichTextKit;
5
6namespace CascadeIDE.Views.SkiaKit;
7
8/// <summary>Единая вёрстка plain-текста (RichTextKit): measure, caret, hit-test. ADR 0123.</summary>
9internal static class SkiaPlainTextLayout
10{
11 public static string NormalizeBody(string? text) => (text ?? "").Replace("\r", "");
12
13 public static RichString BuildRichString(
14 string? text,
15 float maxWidth,
16 float fontSize,
17 float lineHeight,
18 int maxLines,
19 SKColor? textColor = null,
20 string fontFamily = "Segoe UI")
21 {
22 var body = NormalizeBody(text);
23 var rs = new RichString { MaxWidth = maxWidth };
24 rs.FontFamily(fontFamily).FontSize(fontSize);
25 if (textColor.HasValue)
26 rs.TextColor(textColor.Value);
27 if (body.Length > 0)
28 rs.Add(body);
29 if (maxLines > 0 && maxLines < int.MaxValue)
30 rs.MaxHeight = maxLines * lineHeight;
31 return rs;
32 }
33
34 public static SkiaRichTextKitBodyLayout? TryMeasure(
35 string? text,
36 float maxWidth,
37 float fontSize,
38 SKColor color,
39 int maxLines,
40 float lineHeight,
41 string fontFamily = "Segoe UI")
42 {
43 if (maxWidth < 8f)
44 return null;
45
46 var rs = BuildRichString(text, maxWidth, fontSize, lineHeight, maxLines, color, fontFamily);
47 var height = rs.MeasuredHeight;
48 if (height <= 0f)
49 height = string.IsNullOrEmpty(text) ? 0f : lineHeight;
50
51 return new SkiaRichTextKitBodyLayout
52 {
53 Body = text ?? "",
54 MaxWidth = maxWidth,
55 FontSize = fontSize,
56 MaxBodyLines = maxLines,
57 LineHeight = lineHeight,
58 BodyHeight = height,
59 FontFamily = fontFamily,
60 };
61 }
62
63 public static float MeasureBodyHeight(
64 string? text,
65 float maxWidth,
66 float fontSize,
67 float lineHeight,
68 int maxLines = int.MaxValue,
69 string fontFamily = "Segoe UI")
70 {
71 var layout = TryMeasure(
72 text,
73 maxWidth,
74 fontSize,
75 new SKColor(220, 225, 235),
76 maxLines,
77 lineHeight,
78 fontFamily);
79 return layout?.BodyHeight ?? lineHeight;
80 }
81
82 public static bool TryGetCaretLine(
83 string? text,
84 int caretIndex,
85 float maxWidth,
86 float fontSize,
87 float lineHeight,
88 SKPoint origin,
89 out float x,
90 out float yTop,
91 out float yBottom,
92 int maxLines = int.MaxValue,
93 string fontFamily = "Segoe UI")
94 {
95 x = yTop = yBottom = 0f;
96 if (maxWidth < 8f || caretIndex < 0)
97 return false;
98
99 var body = NormalizeBody(text);
100 caretIndex = Math.Clamp(caretIndex, 0, body.Length);
101
102 var rs = BuildRichString(body, maxWidth, fontSize, lineHeight, maxLines, fontFamily: fontFamily);
103 var caretInfo = rs.GetCaretInfo(new CaretPosition(caretIndex, altPosition: false));
104 if (caretInfo.IsNone)
105 return false;
106
107 var caretRect = caretInfo.CaretRectangle;
108 x = origin.X + caretInfo.CaretXCoord;
109 yTop = origin.Y + caretRect.Top + CaretVerticalPad;
110 yBottom = origin.Y + caretRect.Bottom - CaretVerticalPad;
111 if (yBottom <= yTop)
112 yBottom = yTop + Math.Max(lineHeight - CaretVerticalPad * 2f, 8f);
113 return true;
114 }
115
116 public static bool TryHitTestCaretIndex(
117 string? text,
118 float localX,
119 float localY,
120 float maxWidth,
121 float fontSize,
122 float lineHeight,
123 out int caretIndex,
124 int maxLines = int.MaxValue,
125 string fontFamily = "Segoe UI")
126 {
127 caretIndex = 0;
128 if (maxWidth < 8f)
129 return false;
130
131 var body = NormalizeBody(text);
132 var rs = BuildRichString(body, maxWidth, fontSize, lineHeight, maxLines, fontFamily: fontFamily);
133 var hit = rs.HitTest(localX, localY);
134 if (hit.IsNone)
135 return false;
136
137 caretIndex = Math.Clamp(hit.CaretPosition.CodePointIndex, 0, body.Length);
138 return true;
139 }
140
141 /// <summary>Однострочный caret X (navigator search, CCL).</summary>
142 public static float MeasurePrefixWidth(string prefix, float fontSize, string fontFamily = "Segoe UI")
143 {
144 if (prefix.Length == 0)
145 return 0f;
146
147 using var font = SkiaKitFonts.CreateUi(fontSize);
148 return font.MeasureText(prefix);
149 }
150
151 private const float CaretVerticalPad = 2f;
152}
153
View only · write via MCP/CIDE