lightweight variant (#1)

Reviewed-on: #1
Co-authored-by: Aleksandr Neychev <alexnejchev73@gmail.com>
This commit was merged in pull request #1.
This commit is contained in:
2026-08-12 13:37:30 +00:00
committed by alex
parent a0d3098fe4
commit 55ac8e6556
147 changed files with 4055 additions and 2349 deletions
+237
View File
@@ -0,0 +1,237 @@
using System.Runtime.InteropServices;
#if CARET_UI_AUTOMATION
using System.Windows.Automation;
using System.Windows.Automation.Text;
#endif
using Accessibility;
namespace CursorLang.Core.Interop;
/// <summary>
/// Locates the caret in the active input field — including one in another application.
/// </summary>
/// <remarks>
/// 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 <c>CARET_UI_AUTOMATION</c>: 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.
/// </remarks>
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;
/// <summary>
/// The caret rectangle in screen pixels, or <c>null</c> when the active
/// application does not report its position.
/// </summary>
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);
}
/// <summary>
/// Filters out obviously wrong coordinates.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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);
}
/// <summary>
/// The same check over plain numbers: the input window bounds and the scale of
/// its monitor are already known.
/// </summary>
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
/// <summary>How long we wait for another application to answer over UI Automation.</summary>
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 we show the popup at the cursor
Task<PopupWindowNative.Rect?> 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 cursor
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;
}
@@ -0,0 +1,41 @@
using System.Runtime.InteropServices;
namespace CursorLang.Core.Interop;
/// <summary>
/// Input details of the active application: which window holds keyboard focus
/// and where the caret is.
/// </summary>
internal static class ForegroundInputNative
{
[StructLayout(LayoutKind.Sequential)]
internal struct GuiThreadInfo
{
public int cbSize;
public uint flags;
public IntPtr hwndActive;
public IntPtr hwndFocus;
public IntPtr hwndCapture;
public IntPtr hwndMenuOwner;
public IntPtr hwndMoveSize;
public IntPtr hwndCaret;
public PopupWindowNative.Rect rcCaret;
}
[DllImport("user32.dll")]
private static extern bool GetGUIThreadInfo(uint idThread, ref GuiThreadInfo lpgui);
/// <summary>
/// The input state of the foreground thread.
/// </summary>
/// <remarks>
/// The zero thread identifier is not accidental: in modern applications the
/// top-level window and the input window live in different threads, and the
/// question has to be about the foreground as a whole.
/// </remarks>
internal static bool TryGetInfo(out GuiThreadInfo info)
{
info = new GuiThreadInfo { cbSize = Marshal.SizeOf<GuiThreadInfo>() };
return GetGUIThreadInfo(0, ref info);
}
}
@@ -0,0 +1,26 @@
using System.Runtime.InteropServices;
namespace CursorLang.Core.Interop;
/// <summary>
/// The right to bring a window to the foreground.
/// </summary>
internal static class ForegroundPermissionNative
{
/// <summary>ASFW_ANY — any process gets the right.</summary>
private const uint AnyProcess = 0xFFFFFFFF;
/// <summary>
/// Gives up our right to bring a window to the foreground in favour of other processes.
/// </summary>
/// <remarks>
/// Windows does not let just anyone change the foreground window: you have to be
/// the process the user interacted with last. An instance started long ago is not
/// one of those, and its window will come up only if the right is shared by the
/// process the user has just launched.
/// </remarks>
internal static void GrantToAnyProcess() => AllowSetForegroundWindow(AnyProcess);
[DllImport("user32.dll")]
private static extern bool AllowSetForegroundWindow(uint dwProcessId);
}
@@ -0,0 +1,85 @@
using System.Runtime.InteropServices;
namespace CursorLang.Core.Interop;
/// <summary>
/// Win32 API for reading the layout of the active application.
/// </summary>
internal static class KeyboardLayoutNative
{
private const uint WmInputLangChangeRequest = 0x0050;
/// <summary>Take the next layout from the system list.</summary>
private static readonly IntPtr InputLangChangeForward = new(0x0002);
/// <summary>HKL_NEXT — the same request in the language of older Windows versions.</summary>
private static readonly IntPtr HklNext = new(1);
[DllImport("user32.dll")]
internal static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr lpdwProcessId);
[DllImport("user32.dll")]
private static extern IntPtr GetKeyboardLayout(uint idThread);
[DllImport("user32.dll")]
private static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
/// <summary>
/// The layout the user is currently typing with.
/// </summary>
internal static int GetActiveLocaleId() => GetLocaleIdOf(GetInputWindow());
/// <summary>
/// Asks the active application to switch to the next layout from the system list.
/// </summary>
/// <remarks>
/// Synthesizing a system shortcut such as Alt+Shift will not do: the user can
/// reassign it in the Windows settings or turn it off entirely. A request sent as
/// a message does not depend on those settings and works in another process.
/// </remarks>
internal static void RequestNextLayout()
{
IntPtr target = GetInputWindow();
if (target == IntPtr.Zero)
{
return;
}
// Both parameters mean the same thing: different Windows versions and different
// UI frameworks look either at the flag or at lParam
PostMessage(target, WmInputLangChangeRequest, InputLangChangeForward, HklNext);
}
/// <summary>
/// The window that owns keyboard input.
/// </summary>
/// <remarks>
/// We ask the window with keyboard focus rather than the foreground window: in
/// Windows 11 Notepad, the Start menu and other WinUI applications the input field
/// lives in a separate thread, and the layout changes only for that thread. For the
/// main window's thread it stays the same, and the switch goes unnoticed.
/// </remarks>
private static IntPtr GetInputWindow()
{
if (ForegroundInputNative.TryGetInfo(out ForegroundInputNative.GuiThreadInfo info) &&
info.hwndFocus != IntPtr.Zero)
{
return info.hwndFocus;
}
return GetForegroundWindow();
}
/// <summary>
/// The locale identifier for a window. In Windows the layout is bound to a thread,
/// so this reveals it for any application, not only for our own.
/// </summary>
internal static int GetLocaleIdOf(IntPtr hWnd)
{
uint threadId = GetWindowThreadProcessId(hWnd, IntPtr.Zero);
return (int)GetKeyboardLayout(threadId).ToInt64() & 0xFFFF;
}
}
@@ -0,0 +1,130 @@
using System.Runtime.InteropServices;
namespace CursorLang.Core.Interop;
/// <summary>
/// A system keyboard hook (WH_KEYBOARD_LL): sees key presses in every application
/// and can keep them from going any further.
/// </summary>
/// <remarks>
/// Only the low-level hook is installed: a regular WH_KEYBOARD requires injecting a
/// DLL into other processes, which is impossible for managed code. The callback
/// arrives on the thread that installed the hook, and that thread must pump a message
/// loop — hence the requirement to install the hook from the user interface thread.
/// Returning control must not be delayed: once the system timeout expires, Windows
/// silently removes the hook.
/// </remarks>
internal sealed class LowLevelKeyboardHook : IDisposable
{
/// <summary>
/// A key event handler. Returns <c>true</c> when the event must be swallowed —
/// then the foreground application will not see it.
/// </summary>
internal delegate bool KeyFilter(int virtualKey, bool isKeyDown);
private const int WhKeyboardLowLevel = 13;
private const int HcAction = 0;
private const int WmKeyDown = 0x0100;
private const int WmKeyUp = 0x0101;
private const int WmSysKeyDown = 0x0104;
private const int WmSysKeyUp = 0x0105;
/// <summary>The event came from SendInput rather than from a real key press.</summary>
private const uint LowLevelKeyHookFlagInjected = 0x10;
private readonly KeyFilter _filter;
// The delegate lives in a field not for convenience: the only reference to it is
// held by Win32, which the garbage collector knows nothing about, and without the
// field the hook stops working after a random amount of time
private readonly HookProc _callback;
private IntPtr _handle;
internal LowLevelKeyboardHook(KeyFilter filter)
{
_filter = filter;
_callback = OnHookEvent;
}
private delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam);
internal bool IsInstalled => _handle != IntPtr.Zero;
/// <summary>
/// Installs the hook. Returns <c>false</c> when the system refuses.
/// </summary>
internal bool Install()
{
if (_handle != IntPtr.Zero)
{
return true;
}
_handle = SetWindowsHookEx(WhKeyboardLowLevel, _callback, GetModuleHandle(null), 0);
return _handle != IntPtr.Zero;
}
internal void Uninstall()
{
if (_handle == IntPtr.Zero)
{
return;
}
UnhookWindowsHookEx(_handle);
_handle = IntPtr.Zero;
}
public void Dispose() => Uninstall();
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll")]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int code, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr GetModuleHandle(string? lpModuleName);
private IntPtr OnHookEvent(int code, IntPtr wParam, IntPtr lParam)
{
if (code != HcAction)
{
return CallNextHookEx(_handle, code, wParam, lParam);
}
var message = (int)wParam;
bool isKeyDown = message is WmKeyDown or WmSysKeyDown;
bool isKeyUp = message is WmKeyUp or WmSysKeyUp;
var data = Marshal.PtrToStructure<KeyboardHookData>(lParam);
// Synthetic input comes from on-screen keyboards, text expanders and
// automation tools: overriding what they do is none of our business
bool injected = (data.flags & LowLevelKeyHookFlagInjected) != 0;
if ((isKeyDown || isKeyUp) && !injected && _filter((int)data.vkCode, isKeyDown))
{
// A non-zero result instead of CallNextHookEx breaks the chain: the event
// will reach neither the application nor the Windows caps-lock handler
return 1;
}
return CallNextHookEx(_handle, code, wParam, lParam);
}
[StructLayout(LayoutKind.Sequential)]
private struct KeyboardHookData
{
public uint vkCode;
public uint scanCode;
public uint flags;
public uint time;
public IntPtr dwExtraInfo;
}
}
@@ -0,0 +1,38 @@
using System.Runtime.InteropServices;
namespace CursorLang.Core.Interop;
/// <summary>
/// Answers whether the application runs from an MSIX package.
/// </summary>
/// <remarks>
/// The same application runs both installed from the Store and simply unpacked
/// into a folder. Some Windows features — startup through <c>StartupTask</c>, for
/// instance — are available only to a package, and reaching for them without a
/// check is not allowed: outside a package they throw.
/// </remarks>
internal static class PackageIdentityNative
{
/// <summary>APPMODEL_ERROR_NO_PACKAGE — the process runs outside a package.</summary>
private const int NoPackage = 15700;
/// <summary>
/// The application runs from an MSIX package.
/// </summary>
/// <remarks>
/// The value is computed once: it cannot change during the lifetime of
/// the process.
/// </remarks>
internal static bool IsPackaged { get; } = DetectPackage();
private static bool DetectPackage()
{
// The answer comes from the return code rather than from the name itself, so
// no buffer is needed: with zero length a package replies complaining about space
uint length = 0;
return GetCurrentPackageFullName(ref length, IntPtr.Zero) != NoPackage;
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern int GetCurrentPackageFullName(ref uint packageFullNameLength, IntPtr packageFullName);
}
@@ -0,0 +1,112 @@
using System.Runtime.InteropServices;
namespace CursorLang.Core.Interop;
/// <summary>
/// Win32 API for the popup window: styles, positioning near the cursor
/// and the scale of the monitor the cursor is on.
/// </summary>
internal static class PopupWindowNative
{
[StructLayout(LayoutKind.Sequential)]
internal struct Point
{
public int X;
public int Y;
}
[DllImport("user32.dll")]
private static extern bool GetCursorPos(out Point lpPoint);
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter,
int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
private static extern IntPtr MonitorFromPoint(Point pt, uint dwFlags);
[DllImport("shcore.dll")]
private static extern int GetDpiForMonitor(IntPtr hMonitor, int dpiType, out uint dpiX, out uint dpiY);
[DllImport("user32.dll")]
private static extern IntPtr MonitorFromWindow(IntPtr hWnd, uint dwFlags);
[DllImport("user32.dll")]
private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MonitorInfo lpmi);
[StructLayout(LayoutKind.Sequential)]
internal struct Rect
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
[StructLayout(LayoutKind.Sequential)]
private struct MonitorInfo
{
public int cbSize;
public Rect rcMonitor;
public Rect rcWork;
public uint dwFlags;
}
private const uint SWP_NOSIZE = 0x0001;
private const uint SWP_NOZORDER = 0x0004;
private const uint SWP_NOACTIVATE = 0x0010;
private const uint MONITOR_DEFAULTTONEAREST = 2;
private const int MDT_EFFECTIVE_DPI = 0;
internal static Point GetCursorPosition()
{
GetCursorPos(out Point cursor);
return cursor;
}
/// <summary>
/// Moves the window to a screen point without changing its size or z-order.
/// The coordinates are physical pixels: monitors have different scaling, while
/// Window.Left/Top are converted using the DPI of the monitor the window is on
/// right now, which misses the target on a neighbouring monitor.
/// </summary>
internal static void MoveTo(IntPtr hWnd, int x, int y)
{
SetWindowPos(hWnd, IntPtr.Zero, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
}
/// <summary>The scale of the monitor the point is on (1.0 at 96 DPI).</summary>
internal static double GetScaleAt(Point point) =>
GetScaleOf(MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST));
/// <summary>
/// The work area of the monitor holding the active window — without the taskbar —
/// and its scale. That is the monitor the user is working on right now.
/// </summary>
internal static (Rect WorkArea, double Scale) GetActiveMonitorWorkArea()
{
IntPtr monitor = MonitorFromWindow(GetForegroundWindow(), MONITOR_DEFAULTTONEAREST);
var info = new MonitorInfo { cbSize = Marshal.SizeOf<MonitorInfo>() };
if (!GetMonitorInfo(monitor, ref info))
{
return (new Rect(), 1.0);
}
return (info.rcWork, GetScaleOf(monitor));
}
private static double GetScaleOf(IntPtr monitor)
{
if (GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, out uint dpiX, out _) < 0)
{
return 1.0;
}
return dpiX / 96.0;
}
}