using System.Runtime.InteropServices; #if CARET_UI_AUTOMATION using System.Windows.Automation; using System.Windows.Automation.Text; #endif using Accessibility; namespace CursorLang.Core.Interop; /// /// Locates the caret in the active input field — including one in another application. /// /// /// There is no single way to do it: classic Win32 applications create a system caret, /// while Chrome, Electron and others draw it themselves and report its position only /// through accessibility interfaces. So we ask the system first, then the application. /// /// The UI Automation step is behind CARET_UI_AUTOMATION: it is the one part of /// the background process that reaches into the WPF half of the desktop runtime — /// TextPatternRange hands its rectangles back as System.Windows.Rect, which lives in /// WindowsBase — and it was measured at +3.9 MB private. It is also the last of the /// three steps and rarely reached. See the switch in CursorLang.Core.csproj. /// internal static class CaretNative { [DllImport("user32.dll")] private static extern bool ClientToScreen(IntPtr hWnd, ref PopupWindowNative.Point lpPoint); [DllImport("oleacc.dll")] private static extern int AccessibleObjectFromWindow(IntPtr hwnd, uint dwObjectId, ref Guid riid, out IAccessible ppvObject); [DllImport("user32.dll")] private static extern bool GetWindowRect(IntPtr hWnd, out PopupWindowNative.Rect lpRect); [DllImport("user32.dll")] private static extern uint GetDpiForWindow(IntPtr hWnd); private const uint OBJID_CARET = 0xFFFFFFF8; private const int CHILDID_SELF = 0; /// /// The caret rectangle in screen pixels, or null when the active /// application does not report its position. /// internal static PopupWindowNative.Rect? TryGetCaretRect() { if (!ForegroundInputNative.TryGetInfo(out ForegroundInputNative.GuiThreadInfo info)) { return null; } PopupWindowNative.Rect? caret = TryGetSystemCaret(info) ?? TryGetAccessibleCaret(info.hwndFocus) ?? TryGetAutomationCaret(); return caret is null ? null : Validate(caret.Value, info.hwndFocus); } /// /// Filters out obviously wrong coordinates. /// /// /// Some applications report the caret position in their own coordinate system or /// without accounting for display scaling, and the popup ends up far from the input /// field. The caret must be inside the input window — that is what we check, and /// before giving up we try to read the coordinates as unscaled ones. /// private static PopupWindowNative.Rect? Validate(PopupWindowNative.Rect caret, IntPtr hwndFocus) { if (hwndFocus == IntPtr.Zero || !GetWindowRect(hwndFocus, out PopupWindowNative.Rect window)) { return caret; } return Validate(caret, window, GetDpiForWindow(hwndFocus) / 96.0); } /// /// The same check over plain numbers: the input window bounds and the scale of /// its monitor are already known. /// internal static PopupWindowNative.Rect? Validate( PopupWindowNative.Rect caret, PopupWindowNative.Rect window, double scale) { if (IsInside(caret, window)) { return caret; } var scaled = new PopupWindowNative.Rect { Left = (int)(caret.Left * scale), Top = (int)(caret.Top * scale), Right = (int)(caret.Right * scale), Bottom = (int)(caret.Bottom * scale), }; return IsInside(scaled, window) ? scaled : null; } internal static bool IsInside(PopupWindowNative.Rect inner, PopupWindowNative.Rect outer) => inner.Left >= outer.Left && inner.Right <= outer.Right && inner.Top >= outer.Top && inner.Bottom <= outer.Bottom; #if CARET_UI_AUTOMATION /// How long we wait for another application to answer over UI Automation. private static readonly TimeSpan AutomationTimeout = TimeSpan.FromMilliseconds(150); // Browsers and other applications with their own rendering engines draw the caret // themselves and report its position only through UI Automation. The request goes // into another process, so it is the slowest one and comes last private static PopupWindowNative.Rect? TryGetAutomationCaret() { // A hung application must not hang the popup along with it: we wait for the // answer for a limited time, otherwise the popup goes to the fixed point Task query = Task.Run(QueryAutomationCaret); return query.Wait(AutomationTimeout) ? query.Result : null; } private static PopupWindowNative.Rect? QueryAutomationCaret() { try { AutomationElement focused = AutomationElement.FocusedElement; if (focused is null || !focused.TryGetCurrentPattern(TextPattern.Pattern, out object pattern)) { return null; } TextPatternRange[] selection = ((TextPattern)pattern).GetSelection(); if (selection.Length == 0) { return null; } // The caret has an empty selection and therefore no rectangle, // so we expand it to the nearest character TextPatternRange range = selection[0].Clone(); range.ExpandToEnclosingUnit(TextUnit.Character); System.Windows.Rect[] rectangles = range.GetBoundingRectangles(); if (rectangles.Length == 0 || rectangles[0].Height <= 0) { return null; } System.Windows.Rect caret = rectangles[0]; return new PopupWindowNative.Rect { Left = (int)caret.Left, Top = (int)caret.Top, Right = (int)caret.Right, Bottom = (int)caret.Bottom, }; } catch (Exception e) when (e is ElementNotAvailableException or InvalidOperationException or COMException) { // The application closed or stopped responding — that must not take the popup down return null; } } #else // Built without UI Automation: Chromium and Electron keep the system caret and MSAA // steps above, and where those stay silent the popup falls back to the fixed point private static PopupWindowNative.Rect? TryGetAutomationCaret() => null; #endif // The system caret: its coordinates come relative to the window that owns it private static PopupWindowNative.Rect? TryGetSystemCaret(ForegroundInputNative.GuiThreadInfo info) { if (info.hwndCaret == IntPtr.Zero || IsEmpty(info.rcCaret)) { return null; } var topLeft = new PopupWindowNative.Point { X = info.rcCaret.Left, Y = info.rcCaret.Top }; var bottomRight = new PopupWindowNative.Point { X = info.rcCaret.Right, Y = info.rcCaret.Bottom }; if (!ClientToScreen(info.hwndCaret, ref topLeft) || !ClientToScreen(info.hwndCaret, ref bottomRight)) { return null; } return new PopupWindowNative.Rect { Left = topLeft.X, Top = topLeft.Y, Right = bottomRight.X, Bottom = bottomRight.Y, }; } // The caret through accessibility interfaces: this is where browsers and Electron land private static PopupWindowNative.Rect? TryGetAccessibleCaret(IntPtr hwndFocus) { if (hwndFocus == IntPtr.Zero) { return null; } Guid iid = typeof(IAccessible).GUID; if (AccessibleObjectFromWindow(hwndFocus, OBJID_CARET, ref iid, out IAccessible caret) != 0) { return null; } try { caret.accLocation(out int left, out int top, out int width, out int height, CHILDID_SELF); var rect = new PopupWindowNative.Rect { Left = left, Top = top, Right = left + width, Bottom = top + height, }; return IsEmpty(rect) ? null : rect; } catch (COMException) { // The application declared support but did not report the position return null; } finally { Marshal.ReleaseComObject(caret); } } // When there is no caret, its rectangle comes back with zero height. // We judge by height alone: zero coordinates are a normal start of an empty field internal static bool IsEmpty(PopupWindowNative.Rect rect) => rect.Bottom - rect.Top <= 0; }