← All source files

V18 / 8d896b8a86e5

GuiTray.cs

Interfaces/Gui/LilithGui/GuiTray.cs

Unchanged from the preceding version
CS
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);
}