V17 / d6d373561692
GuiTray.cs
Interfaces/Gui/LilithGui/GuiTray.cs
Unchanged from the preceding versionCS
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace Lilith.Gui;
// A small native message loop keeps the tray independent of the web-host threads.
internal sealed class GuiTray : IDisposable
{
private const uint Callback = 0x8001;
private readonly Action _open;
private readonly Thread _thread;
private readonly WindowProc _procedure;
private readonly ManualResetEventSlim _started = new();
private IntPtr _window, _icon;
private Exception? _error;
private readonly string _className = "LilithTray" + Guid.NewGuid().ToString("N");
internal GuiTray(string url):this(()=>Process.Start(new ProcessStartInfo(url){UseShellExecute=true})) { }
internal GuiTray(Action open)
{
_open = open;
_procedure = OnMessage;
_thread = new Thread(Run) { IsBackground = true, Name = "Lilith tray" };
_thread.Start();
_started.Wait();
if (_error is not null) { _thread.Join(); _started.Dispose(); throw new InvalidOperationException("Tray could not start.", _error); }
}
private void Run()
{
var instance = GetModuleHandle(null);
try
{
var cls = new WindowClass { procedure = _procedure, instance = instance, className = _className };
if (RegisterClass(ref cls) == 0) throw new System.ComponentModel.Win32Exception();
_window = CreateWindowEx(0, _className, "Lilith", 0, 0, 0, 0, 0, IntPtr.Zero, IntPtr.Zero, instance, IntPtr.Zero);
if (_window == IntPtr.Zero) throw new System.ComponentModel.Win32Exception();
_icon = CreateLogo(instance);
var data = Data();
data.flags = 1 | 2 | 4; data.callback = Callback; data.icon = _icon; data.tip = "Lilith — Local Intelligence";
if (!Shell_NotifyIcon(0, ref data)) throw new InvalidOperationException("Windows Shell rejected notification-icon registration.");
_started.Set();
while (GetMessage(out var message, IntPtr.Zero, 0, 0) > 0) { TranslateMessage(ref message); DispatchMessage(ref message); }
}
catch (Exception ex) { _error = ex; }
finally
{
_started.Set();
if (_window != IntPtr.Zero) { var data = Data(); Shell_NotifyIcon(2, ref data); DestroyWindow(_window); }
if (_icon != IntPtr.Zero) DestroyIcon(_icon);
UnregisterClass(_className, instance);
}
}
private NotifyData Data() => new() { size = (uint)Marshal.SizeOf<NotifyData>(), window = _window, id = 1 };
private IntPtr OnMessage(IntPtr window, uint message, IntPtr wParam, IntPtr lParam)
{
if (message == Callback && ((int)lParam == 0x202 || (int)lParam == 0x205))
{
var menu = CreatePopupMenu();
try
{
AppendMenu(menu, 0, 1, "Open Lilith");
GetCursorPos(out var point); SetForegroundWindow(window);
if (TrackPopupMenu(menu, 0x100 | 2, point.x, point.y, 0, window, IntPtr.Zero) == 1)
try { _open(); }
catch (Exception ex) { Console.WriteLine("Could not open GUI: " + ex.Message); }
PostMessage(window, 0, IntPtr.Zero, IntPtr.Zero);
}
finally { DestroyMenu(menu); }
return IntPtr.Zero;
}
if (message == 0x10) { PostQuitMessage(0); return IntPtr.Zero; }
return DefWindowProc(window, message, wParam, lParam);
}
private static IntPtr CreateLogo(IntPtr instance)
{
// Match wwwroot/favicon.svg, including its dark rounded background.
var mask = Enumerable.Repeat((byte)255, 128).ToArray();
var pixels = new byte[32 * 32 * 4];
for (int y = 0; y < 32; y++) for (int x = 0; x < 32; x++)
{
double sx = (x + .5) * 1.5, sy = (y + .5) * 1.5;
double cornerX = Math.Clamp(sx, 8, 40), cornerY = Math.Clamp(sy, 8, 40);
bool background = (sx - cornerX) * (sx - cornerX) + (sy - cornerY) * (sy - cornerY) <= 64;
bool ink = (sx >= 7 && sx < 15 && sy >= 12 && sy < 45)
|| (sx >= 7 && sx < 40 && sy >= 37 && sy < 45)
|| ((sx - 34) * (sx - 34) + (sy - 12) * (sy - 12) <= 64);
if (!background) continue;
// CreateIcon consumes these device-dependent mask rows top to bottom.
mask[y * 4 + x / 8] &= (byte)~(128 >> (x % 8));
int offset = (y * 32 + x) * 4;
pixels[offset] = ink ? (byte)117 : (byte)14;
pixels[offset + 1] = ink ? (byte)246 : (byte)16;
pixels[offset + 2] = ink ? (byte)214 : (byte)11;
pixels[offset + 3] = 255;
}
var icon = CreateIcon(instance, 32, 32, 1, 32, mask, pixels);
return icon != IntPtr.Zero ? icon : throw new System.ComponentModel.Win32Exception();
}
public void Dispose() { if (_thread.IsAlive) { PostMessage(_window, 0x10, IntPtr.Zero, IntPtr.Zero); _thread.Join(); } _started.Dispose(); }
private delegate IntPtr WindowProc(IntPtr window, uint message, IntPtr wParam, IntPtr lParam);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct WindowClass { public uint style; public WindowProc procedure; public int classExtra, windowExtra; public IntPtr instance, icon, cursor, background; public string? menu; public string className; }
[StructLayout(LayoutKind.Sequential)] private struct Point { public int x, y; }
[StructLayout(LayoutKind.Sequential)] private struct Message { public IntPtr window; public uint message; public UIntPtr wParam; public IntPtr lParam; public uint time; public Point point; public uint privateData; }
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct NotifyData
{
public uint size; public IntPtr window; public uint id, flags, callback; public IntPtr icon;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string? tip;
public uint state, stateMask;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string? info;
public uint timeout;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] public string? title;
public uint infoFlags; public Guid guid; public IntPtr balloon;
}
[DllImport("kernel32", CharSet = CharSet.Unicode)] private static extern IntPtr GetModuleHandle(string? name);
[DllImport("user32", CharSet = CharSet.Unicode)] private static extern ushort RegisterClass(ref WindowClass cls);
[DllImport("user32", CharSet = CharSet.Unicode)] private static extern bool UnregisterClass(string name, IntPtr instance);
[DllImport("user32", CharSet = CharSet.Unicode)] private static extern IntPtr CreateWindowEx(uint ex, string cls, string title, uint style, int x, int y, int w, int h, IntPtr parent, IntPtr menu, IntPtr instance, IntPtr param);
[DllImport("user32", CharSet = CharSet.Unicode)] private static extern IntPtr DefWindowProc(IntPtr window, uint message, IntPtr w, IntPtr l);
[DllImport("user32")] private static extern int GetMessage(out Message message, IntPtr window, uint min, uint max);
[DllImport("user32")] private static extern bool TranslateMessage(ref Message message);
[DllImport("user32", CharSet = CharSet.Unicode)] private static extern IntPtr DispatchMessage(ref Message message);
[DllImport("user32")] private static extern bool PostMessage(IntPtr window, uint message, IntPtr w, IntPtr l);
[DllImport("user32")] private static extern void PostQuitMessage(int code);
[DllImport("user32")] private static extern bool DestroyWindow(IntPtr window);
[DllImport("shell32", CharSet = CharSet.Unicode)] private static extern bool Shell_NotifyIcon(uint action, ref NotifyData data);
[DllImport("user32")] private static extern IntPtr CreateIcon(IntPtr instance, int width, int height, byte planes, byte bits, byte[] andMask, byte[] xorMask);
[DllImport("user32")] private static extern bool DestroyIcon(IntPtr icon);
[DllImport("user32")] private static extern IntPtr CreatePopupMenu();
[DllImport("user32", CharSet = CharSet.Unicode)] private static extern bool AppendMenu(IntPtr menu, uint flags, uint id, string text);
[DllImport("user32")] private static extern uint TrackPopupMenu(IntPtr menu, uint flags, int x, int y, int reserved, IntPtr window, IntPtr rect);
[DllImport("user32")] private static extern bool DestroyMenu(IntPtr menu);
[DllImport("user32")] private static extern bool GetCursorPos(out Point point);
[DllImport("user32")] private static extern bool SetForegroundWindow(IntPtr window);
}