| 1 | #nullable enable |
| 2 | |
| 3 | using Avalonia; |
| 4 | using Avalonia.Input; |
| 5 | using CascadeIDE.Features.Chat; |
| 6 | using CascadeIDE.Views.Chat; |
| 7 | using CascadeIDE.Views.Chat.Skia; |
| 8 | using CascadeIDE.Views.SkiaKit; |
| 9 | using SkiaSharp; |
| 10 | |
| 11 | namespace CascadeIDE.Views; |
| 12 | |
| 13 | public partial class SkiaChatSurfaceControl |
| 14 | { |
| 15 | private bool TryDispatchPointerPress(Point point, PointerPressedEventArgs e) |
| 16 | { |
| 17 | var index = _chatHits.FindIndex(point); |
| 18 | if (index < 0 || !_chatHits.TryGetHit(index, out var hit)) |
| 19 | return false; |
| 20 | |
| 21 | if (SkiaChatHitRegistry.IsChromeAction(hit)) |
| 22 | return dispatchChromePointerPress(hit, e); |
| 23 | |
| 24 | return dispatchFeedPointerPress(hit, point, e); |
| 25 | } |
| 26 | |
| 27 | private bool dispatchChromePointerPress(in SkiaChatHit hit, PointerPressedEventArgs e) |
| 28 | { |
| 29 | switch (hit.PointerAction) |
| 30 | { |
| 31 | case SkiaChatPointerAction.OverviewToggle: |
| 32 | OverviewMode = !OverviewMode; |
| 33 | return true; |
| 34 | case SkiaChatPointerAction.ComposerSend: |
| 35 | if (!ShowIntercomComposer) |
| 36 | return false; |
| 37 | Focus(); |
| 38 | if (IsComposerEnabled) |
| 39 | SendRequested?.Invoke(this, EventArgs.Empty); |
| 40 | return true; |
| 41 | case SkiaChatPointerAction.SlashPopup when ShowIntercomComposer && _slashPopupBounds.Width > 0: |
| 42 | { |
| 43 | var popupPoint = e.GetPosition(this); |
| 44 | var row = SkiaPopupList.HitTestRow( |
| 45 | _slashPopupBounds, |
| 46 | (float)popupPoint.X, |
| 47 | (float)popupPoint.Y, |
| 48 | _slashRows.Count, |
| 49 | _slashPopupScrollOffset, |
| 50 | ShowSlashHierarchyHeader); |
| 51 | if (row < 0) |
| 52 | return false; |
| 53 | SelectedSlashSuggestionIndex = row; |
| 54 | ComposerKeyDown?.Invoke(this, new IntercomComposerKeyEventArgs(IntercomComposerKeyKind.CommitSlashSuggestion)); |
| 55 | return true; |
| 56 | } |
| 57 | case SkiaChatPointerAction.CommandLineFocus: |
| 58 | if (!ShowIntercomComposer || !ShowCockpitCommandLine) |
| 59 | return false; |
| 60 | ClearNavigatorSearchFocus(); |
| 61 | _commandLineFocused = true; |
| 62 | Focus(); |
| 63 | var cclPoint = e.GetPosition(this); |
| 64 | TryPlaceCommandLineCaretAtPoint( |
| 65 | (float)cclPoint.X, |
| 66 | (float)cclPoint.Y, |
| 67 | e.KeyModifiers.HasFlag(KeyModifiers.Shift)); |
| 68 | return true; |
| 69 | case SkiaChatPointerAction.ComposerFocus: |
| 70 | if (!ShowIntercomComposer) |
| 71 | return false; |
| 72 | ClearNavigatorSearchFocus(); |
| 73 | _commandLineFocused = false; |
| 74 | Focus(); |
| 75 | var composerPoint = e.GetPosition(this); |
| 76 | TryPlaceComposerCaretAtPoint( |
| 77 | (float)composerPoint.X, |
| 78 | (float)composerPoint.Y, |
| 79 | e.KeyModifiers.HasFlag(KeyModifiers.Shift)); |
| 80 | return true; |
| 81 | case SkiaChatPointerAction.TopicNavigatorSearchFocus: |
| 82 | FocusNavigatorSearch(); |
| 83 | var searchPoint = e.GetPosition(this); |
| 84 | TryPlaceNavigatorSearchCaretAtPoint((float)searchPoint.X, (float)searchPoint.Y); |
| 85 | return true; |
| 86 | case SkiaChatPointerAction.TopicTabSelect when hit.SelectThreadId is { } tabThreadId: |
| 87 | if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed) |
| 88 | { |
| 89 | TopicRenameRequested?.Invoke(this, new TopicRenameRequestEventArgs(tabThreadId, showContextMenu: true)); |
| 90 | return true; |
| 91 | } |
| 92 | |
| 93 | if (e.ClickCount >= 2) |
| 94 | { |
| 95 | TopicRenameRequested?.Invoke(this, new TopicRenameRequestEventArgs(tabThreadId, showContextMenu: false)); |
| 96 | return true; |
| 97 | } |
| 98 | |
| 99 | DetailThreadId = tabThreadId; |
| 100 | OverviewMode = false; |
| 101 | return true; |
| 102 | case SkiaChatPointerAction.TopicTabCreate: |
| 103 | TopicCreateRequested?.Invoke(this, EventArgs.Empty); |
| 104 | return true; |
| 105 | case SkiaChatPointerAction.TopicTabOverflow: |
| 106 | OverviewMode = true; |
| 107 | return true; |
| 108 | case SkiaChatPointerAction.TopicNavigatorToggle: |
| 109 | TopicNavigatorToggleRequested?.Invoke(this, EventArgs.Empty); |
| 110 | return true; |
| 111 | default: |
| 112 | return false; |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | private bool dispatchFeedPointerPress(in SkiaChatHit hit, Point point, PointerPressedEventArgs e) |
| 117 | { |
| 118 | if (hit.RevealAttachment is { } attachAnchor) |
| 119 | { |
| 120 | var select = e.KeyModifiers.HasFlag(KeyModifiers.Shift); |
| 121 | AttachmentRevealRequested?.Invoke( |
| 122 | this, |
| 123 | new IntercomAttachmentRevealEventArgs(attachAnchor, select, hit.MessageIndex)); |
| 124 | return true; |
| 125 | } |
| 126 | |
| 127 | if (hit.ResetDetailMode) |
| 128 | { |
| 129 | OverviewMode = true; |
| 130 | return true; |
| 131 | } |
| 132 | |
| 133 | if (hit.SelectThreadId is { } threadId) |
| 134 | { |
| 135 | if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed) |
| 136 | { |
| 137 | TopicRenameRequested?.Invoke(this, new TopicRenameRequestEventArgs(threadId, showContextMenu: true)); |
| 138 | return true; |
| 139 | } |
| 140 | |
| 141 | if (e.ClickCount >= 2) |
| 142 | { |
| 143 | TopicRenameRequested?.Invoke(this, new TopicRenameRequestEventArgs(threadId, showContextMenu: false)); |
| 144 | return true; |
| 145 | } |
| 146 | |
| 147 | DetailThreadId = threadId; |
| 148 | OverviewMode = false; |
| 149 | return true; |
| 150 | } |
| 151 | |
| 152 | if (hit.MessageIndex is not { } messageIndex) |
| 153 | return false; |
| 154 | |
| 155 | if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed) |
| 156 | { |
| 157 | MessageSelectContextRequested?.Invoke(this, messageIndex); |
| 158 | return true; |
| 159 | } |
| 160 | |
| 161 | if (hit.ToggleThinking && e.ClickCount >= 2) |
| 162 | ThinkingToggleRequested?.Invoke(this, messageIndex); |
| 163 | |
| 164 | return true; |
| 165 | } |
| 166 | |
| 167 | private bool TryDispatchPointerWheel(Point point, PointerWheelEventArgs e) |
| 168 | { |
| 169 | if (_chatHits.ContainsPointerAction(point, SkiaChatPointerAction.SlashPopup) |
| 170 | && _slashRows.Count > 0) |
| 171 | { |
| 172 | var deltaRows = e.Delta.Y > 0 ? -1 : e.Delta.Y < 0 ? 1 : 0; |
| 173 | if (deltaRows == 0) |
| 174 | return false; |
| 175 | |
| 176 | _slashPopupScrollOffset = SkiaPopupList.ClampScrollOffset( |
| 177 | _slashPopupScrollOffset + deltaRows, |
| 178 | _slashRows.Count); |
| 179 | return true; |
| 180 | } |
| 181 | |
| 182 | if (_chatHits.ContainsPointerAction(point, SkiaChatPointerAction.CommandLineFocus) |
| 183 | && ShowCockpitCommandLine |
| 184 | && _commandLineFocused) |
| 185 | { |
| 186 | if (TryScrollCommandLine((float)(e.Delta.Y * WheelPixelsPerDelta))) |
| 187 | return true; |
| 188 | return true; |
| 189 | } |
| 190 | |
| 191 | if (_chatHits.ContainsPointerAction(point, SkiaChatPointerAction.ComposerFocus)) |
| 192 | { |
| 193 | if (TryScrollComposer((float)(e.Delta.Y * WheelPixelsPerDelta))) |
| 194 | return true; |
| 195 | return true; |
| 196 | } |
| 197 | |
| 198 | return false; |
| 199 | } |
| 200 | |
| 201 | private void registerChromePointerHits(SKRect overviewButtonBounds, SKRect navigatorToggleBounds) |
| 202 | { |
| 203 | if (navigatorToggleBounds.Width > 0) |
| 204 | { |
| 205 | _chatHits.RegisterControlRect( |
| 206 | SkiaChatHitGeometry.ToControlRect(navigatorToggleBounds), |
| 207 | new SkiaChatHit(null, null, ResetDetailMode: false, PointerAction: SkiaChatPointerAction.TopicNavigatorToggle)); |
| 208 | } |
| 209 | |
| 210 | if (overviewButtonBounds.Width > 0) |
| 211 | { |
| 212 | _chatHits.RegisterControlRect( |
| 213 | SkiaChatHitGeometry.ToControlRect(overviewButtonBounds), |
| 214 | new SkiaChatHit(null, null, ResetDetailMode: false, PointerAction: SkiaChatPointerAction.OverviewToggle)); |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | private void registerTopicNavigatorPointerHits(SkiaIntercomTopicNavigator.LayoutResult layout, float panelLeft) |
| 219 | { |
| 220 | if (layout.SearchBounds.Width > 0) |
| 221 | { |
| 222 | _navigatorSearchBounds = layout.SearchBounds; |
| 223 | var searchBounds = layout.SearchBounds; |
| 224 | if (panelLeft != 0f) |
| 225 | searchBounds.Offset(panelLeft, 0f); |
| 226 | |
| 227 | _chatHits.RegisterControlRect( |
| 228 | SkiaChatHitGeometry.ToControlRect(searchBounds), |
| 229 | new SkiaChatHit(null, null, ResetDetailMode: false, PointerAction: SkiaChatPointerAction.TopicNavigatorSearchFocus)); |
| 230 | } |
| 231 | |
| 232 | // RowHits.Bounds уже в координатах контрола (MapRowBoundsToPanel в Draw). |
| 233 | foreach (var row in layout.RowHits) |
| 234 | { |
| 235 | var bounds = row.Bounds; |
| 236 | if (panelLeft != 0f) |
| 237 | bounds.Offset(panelLeft, 0f); |
| 238 | |
| 239 | _chatHits.RegisterControlRect( |
| 240 | SkiaChatHitGeometry.ToControlRect(bounds), |
| 241 | new SkiaChatHit(null, row.ThreadId, ResetDetailMode: false)); |
| 242 | } |
| 243 | } |
| 244 | |
| 245 | private void registerNavigationPointerHits(SkiaIntercomNavigationChrome.LayoutResult layout) |
| 246 | { |
| 247 | foreach (var tab in layout.TabHits) |
| 248 | { |
| 249 | _chatHits.RegisterControlRect( |
| 250 | SkiaChatHitGeometry.ToControlRect(tab.Bounds), |
| 251 | new SkiaChatHit( |
| 252 | null, |
| 253 | tab.ThreadId, |
| 254 | ResetDetailMode: false, |
| 255 | PointerAction: SkiaChatPointerAction.TopicTabSelect)); |
| 256 | } |
| 257 | |
| 258 | if (layout.CreateButtonBounds.Width > 0) |
| 259 | { |
| 260 | _chatHits.RegisterControlRect( |
| 261 | SkiaChatHitGeometry.ToControlRect(layout.CreateButtonBounds), |
| 262 | new SkiaChatHit(null, null, ResetDetailMode: false, PointerAction: SkiaChatPointerAction.TopicTabCreate)); |
| 263 | } |
| 264 | |
| 265 | if (layout.OverflowBounds is { } overflow && layout.OverflowHiddenCount > 0) |
| 266 | { |
| 267 | _chatHits.RegisterControlRect( |
| 268 | SkiaChatHitGeometry.ToControlRect(overflow), |
| 269 | new SkiaChatHit(null, null, ResetDetailMode: false, PointerAction: SkiaChatPointerAction.TopicTabOverflow)); |
| 270 | } |
| 271 | } |
| 272 | } |
| 273 | |