Forge
csharp4405de34
1#nullable enable
2
3using Avalonia;
4using Avalonia.Input;
5using Avalonia.Input.TextInput;
6using Avalonia.Interactivity;
7using Avalonia.Media;
8using CascadeIDE.Views.Chat.Skia;
9using CascadeIDE.Views.SkiaKit;
10using SkiaSharp;
11
12namespace CascadeIDE.Views;
13
14public partial class SkiaChatSurfaceControl
15{
16 private bool _navigatorSearchFocused;
17 private int _navigatorSearchCaretIndex;
18 private SKRect _navigatorSearchBounds;
19
20 /// <summary>Активен ввод в поле поиска Topic Navigator (Skia).</summary>
21 internal bool IsNavigatorSearchInputActive => _navigatorSearchFocused;
22
23 internal int NavigatorSearchCaretIndex
24 {
25 get => _navigatorSearchCaretIndex;
26 set
27 {
28 var len = (TopicNavigatorSearchQuery ?? "").Length;
29 _navigatorSearchCaretIndex = Math.Clamp(value, 0, len);
30 }
31 }
32
33 /// <summary>Текст поиска изменён до синхронизации с VM.</summary>
34 public event EventHandler? NavigatorSearchDraftChanged;
35
36 internal static void OnTopicNavigatorSearchQueryChanged(
37 SkiaChatSurfaceControl control,
38 AvaloniaPropertyChangedEventArgs e)
39 {
40 if (e.NewValue is string text)
41 control.NavigatorSearchCaretIndex = text.Length;
42
43 control.InvalidateVisual();
44 }
45
46 internal Rect GetNavigatorSearchCaretScreenRect()
47 {
48 if (_navigatorSearchBounds.Width <= 0)
49 return default;
50
51 var pt = Math.Max(10f, IntercomFonts.ResolveChromeSubtitlePt());
52 var text = TopicNavigatorSearchQuery ?? "";
53 var caret = Math.Clamp(_navigatorSearchCaretIndex, 0, text.Length);
54 var prefix = text[..caret];
55 var textLeft = _navigatorSearchBounds.Left + 8f;
56 var caretX = textLeft + SkiaPlainTextLayout.MeasurePrefixWidth(prefix, pt);
57 var midY = _navigatorSearchBounds.MidY + 4f;
58 var lineH = pt + 4f;
59 return new Rect(caretX, midY - lineH * 0.55f, 2, lineH);
60 }
61
62 internal bool TryPlaceNavigatorSearchCaretAtPoint(float x, float y)
63 {
64 if (_navigatorSearchBounds.Width <= 0)
65 return false;
66
67 var pt = Math.Max(10f, IntercomFonts.ResolveChromeSubtitlePt());
68 var text = TopicNavigatorSearchQuery ?? "";
69 var innerLeft = _navigatorSearchBounds.Left + 8f;
70 var innerTop = _navigatorSearchBounds.Top + 4f;
71 var innerWidth = Math.Max(20f, _navigatorSearchBounds.Width - 16f);
72 var innerHeight = Math.Max(12f, _navigatorSearchBounds.Height - 8f);
73 if (x < innerLeft || x > innerLeft + innerWidth || y < innerTop || y > innerTop + innerHeight)
74 return false;
75
76 if (!SkiaPlainTextLayout.TryHitTestCaretIndex(
77 text,
78 x - innerLeft,
79 y - innerTop,
80 innerWidth,
81 pt,
82 innerHeight,
83 out var index,
84 maxLines: 1))
85 index = text.Length;
86
87 NavigatorSearchCaretIndex = index;
88 _textInputClient?.NotifyCursorMoved();
89 ShowComposerCaretSolid();
90 NotifyNavigatorSearchDraftChanged();
91 return true;
92 }
93
94 private void FocusNavigatorSearch()
95 {
96 _navigatorSearchFocused = true;
97 _commandLineFocused = false;
98 NavigatorSearchCaretIndex = (TopicNavigatorSearchQuery ?? "").Length;
99 Focus();
100 StartComposerCaretBlink();
101 _textInputClient?.NotifyTextChanged();
102 _textInputClient?.NotifyCursorMoved();
103 InvalidateVisual();
104 }
105
106 private void ClearNavigatorSearchFocus()
107 {
108 if (!_navigatorSearchFocused)
109 return;
110
111 _navigatorSearchFocused = false;
112 InvalidateVisual();
113 }
114
115 private void NotifyNavigatorSearchDraftChanged()
116 {
117 NavigatorSearchDraftChanged?.Invoke(this, EventArgs.Empty);
118 InvalidateVisual();
119 }
120
121 private void InsertNavigatorSearchText(string text)
122 {
123 if (string.IsNullOrEmpty(text))
124 return;
125
126 if (!IsKeyboardFocusWithin)
127 Focus();
128
129 _navigatorSearchFocused = true;
130 _commandLineFocused = false;
131 var current = TopicNavigatorSearchQuery ?? "";
132 var caret = Math.Clamp(_navigatorSearchCaretIndex, 0, current.Length);
133 TopicNavigatorSearchQuery = current.Insert(caret, text);
134 NavigatorSearchCaretIndex = caret + text.Length;
135 _textInputClient?.NotifyTextChanged();
136 _textInputClient?.NotifyCursorMoved();
137 ShowComposerCaretSolid();
138 NotifyNavigatorSearchDraftChanged();
139 }
140
141 private void DeleteNavigatorSearch(int direction)
142 {
143 var current = TopicNavigatorSearchQuery ?? "";
144 var caret = Math.Clamp(_navigatorSearchCaretIndex, 0, current.Length);
145 if (direction < 0)
146 {
147 if (caret == 0)
148 return;
149
150 TopicNavigatorSearchQuery = current.Remove(caret - 1, 1);
151 NavigatorSearchCaretIndex = caret - 1;
152 }
153 else
154 {
155 if (caret >= current.Length)
156 return;
157
158 TopicNavigatorSearchQuery = current.Remove(caret, 1);
159 NavigatorSearchCaretIndex = caret;
160 }
161
162 _textInputClient?.NotifyTextChanged();
163 _textInputClient?.NotifyCursorMoved();
164 ShowComposerCaretSolid();
165 NotifyNavigatorSearchDraftChanged();
166 }
167
168 private void MoveNavigatorSearchCaret(int delta)
169 {
170 NavigatorSearchCaretIndex = _navigatorSearchCaretIndex + delta;
171 _textInputClient?.NotifyCursorMoved();
172 ShowComposerCaretSolid();
173 InvalidateVisual();
174 }
175
176 private bool TryHandleNavigatorSearchKeyDown(KeyEventArgs e)
177 {
178 if (!_navigatorSearchFocused)
179 return false;
180
181 if (!IsKeyboardFocusWithin)
182 Focus();
183
184 if (e.Key == Key.Escape)
185 {
186 ClearNavigatorSearchFocus();
187 e.Handled = true;
188 return true;
189 }
190
191 if (TryMapComposerTextKey(e, out var textKey))
192 {
193 InsertNavigatorSearchText(textKey);
194 e.Handled = true;
195 return true;
196 }
197
198 switch (e.Key)
199 {
200 case Key.Back:
201 DeleteNavigatorSearch(-1);
202 e.Handled = true;
203 return true;
204 case Key.Delete:
205 DeleteNavigatorSearch(1);
206 e.Handled = true;
207 return true;
208 case Key.Left:
209 MoveNavigatorSearchCaret(-1);
210 e.Handled = true;
211 return true;
212 case Key.Right:
213 MoveNavigatorSearchCaret(1);
214 e.Handled = true;
215 return true;
216 case Key.Home:
217 NavigatorSearchCaretIndex = 0;
218 ShowComposerCaretSolid();
219 e.Handled = true;
220 return true;
221 case Key.End:
222 NavigatorSearchCaretIndex = (TopicNavigatorSearchQuery ?? "").Length;
223 ShowComposerCaretSolid();
224 e.Handled = true;
225 return true;
226 }
227
228 return false;
229 }
230
231 private bool TryHandleNavigatorSearchTextInput(TextInputEventArgs e)
232 {
233 if (!_navigatorSearchFocused || string.IsNullOrEmpty(e.Text))
234 return false;
235
236 InsertNavigatorSearchText(e.Text);
237 e.Handled = true;
238 return true;
239 }
240}
241
View only · write via MCP/CIDE