Files
cursor-lang/CursorLang.Agent/Interop/MenuNative.cs
T
alex 6259dbd6b3
Pull request / build (pull_request) Successful in 53s
lightweight variant
2026-08-12 16:01:54 +05:00

99 lines
3.7 KiB
C#

using System.Runtime.InteropServices;
using CursorLang.Core.Interop;
namespace CursorLang.Agent.Interop;
/// <summary>
/// The system context menu — the one the tray icon raises.
/// </summary>
/// <remarks>
/// A menu built this way is drawn by Windows, so the theme and the language chosen in
/// the application no longer reach it. That is the accepted price of leaving WPF: a WPF
/// <c>ContextMenu</c> costs the whole rendering stack in the background process.
/// </remarks>
internal static class MenuNative
{
private const uint MF_STRING = 0x00000000;
private const uint MF_SEPARATOR = 0x00000800;
private const uint MF_GRAYED = 0x00000001;
private const uint TPM_LEFTALIGN = 0x0000;
private const uint TPM_RIGHTBUTTON = 0x0002;
private const uint TPM_RETURNCMD = 0x0100;
/// <summary>An item of the menu being built.</summary>
/// <param name="Id">What <see cref="Track"/> gives back when the item is chosen.</param>
/// <param name="Caption">The text, or <c>null</c> for a separator.</param>
/// <param name="IsEnabled">A greyed item is shown but cannot be chosen.</param>
internal readonly record struct Item(int Id, string? Caption, bool IsEnabled = true)
{
internal static Item Separator => new(0, null);
}
/// <summary>
/// Raises the menu at a screen point and returns the identifier of the chosen item,
/// or zero when the user dismissed it.
/// </summary>
/// <remarks>
/// <c>TPM_RETURNCMD</c> means the answer comes back from the call itself instead of
/// as a <c>WM_COMMAND</c> later, which keeps the whole menu in one place. The call
/// does not return until the user is done with the menu — that is how a modal menu
/// works, and the message loop keeps running inside it.
///
/// The window is brought to the foreground first and poked with an empty message
/// afterwards: without the first the menu never closes on a click elsewhere, and
/// without the second it stays on screen after the choice is made. Both are
/// long-standing quirks of a menu owned by a window the user cannot see.
/// </remarks>
internal static int Track(IntPtr owner, PopupWindowNative.Point at, IReadOnlyList<Item> items)
{
IntPtr menu = CreatePopupMenu();
if (menu == IntPtr.Zero)
{
return 0;
}
try
{
foreach (Item item in items)
{
if (item.Caption is null)
{
AppendMenu(menu, MF_SEPARATOR, IntPtr.Zero, null);
continue;
}
uint flags = MF_STRING | (item.IsEnabled ? 0 : MF_GRAYED);
AppendMenu(menu, flags, new IntPtr(item.Id), item.Caption);
}
TrayIconNative.BringToForeground(owner);
int chosen = TrackPopupMenuEx(
menu, TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD,
at.X, at.Y, owner, IntPtr.Zero);
WindowNative.PostMessage(owner, WindowNative.WM_NULL, IntPtr.Zero, IntPtr.Zero);
return chosen;
}
finally
{
DestroyMenu(menu);
}
}
[DllImport("user32.dll")]
private static extern IntPtr CreatePopupMenu();
[DllImport("user32.dll")]
private static extern bool DestroyMenu(IntPtr hMenu);
[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "AppendMenuW")]
private static extern bool AppendMenu(IntPtr hMenu, uint uFlags, IntPtr uIDNewItem, string? lpNewItem);
[DllImport("user32.dll")]
private static extern int TrackPopupMenuEx(IntPtr hMenu, uint uFlags, int x, int y,
IntPtr hwnd, IntPtr lptpm);
}