using System.Runtime.InteropServices; using LlamaApp.Llama; using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media; using Windows.Graphics; using Windows.UI; using WinRT.Interop; namespace LlamaApp.Views; /// /// A spotlight-style prompt overlay: a centered, borderless Mica window that /// hosts the running llama server's WebUI for the currently loaded model inside /// a . Summoned by the global /// Alt+Space hotkey (see ) while LlamaApp is /// running. /// /// The overlay is created lazily on first summon and reused thereafter: /// it hides rather than closes, mirroring the tray flyout's lifecycle, so /// re-summoning is instant. Esc and deactivation (clicking away) dismiss it. /// /// The embedded WebUI drives its own prompt + streaming chat against the /// loaded model — the overlay just points a WebView2 at the right model URL /// (http://localhost:{ServerPort}?model={LoadedModelId}, the same URL /// MainWindow opens in the system browser) and lets it run. When no /// model is loaded yet the base WebUI (router mode) is shown, which still lists /// models; the header chip flips to amber + "No model". /// public sealed partial class OverlayWindow : Window { // Overlay covers ~60% of the active monitor's work-area width and ~70% of // its height so the embedded WebUI has room. Tunable: bump these toward 1.0 // (or set fixed constants) to taste. private const double OverlayWidthFraction = 0.6; private const double OverlayHeightFraction = 0.7; // Clamp so the overlay never collapses to nothing on a tiny/rotated screen. // In DIPs, scaled by the target monitor's DPI at summon time — AppWindow // sizes are PHYSICAL pixels, so a raw pixel minimum would shrink to // nothing (in content terms) on high-DPI screens. private const int OverlayMinWidthDips = 520; private const int OverlayMinHeightDips = 360; private const int GWL_EXSTYLE = -20; private const int WS_EX_TOOLWINDOW = 0x00000080; private const int GWL_STYLE = -16; private const long WS_POPUP = 0x80000000L; private bool _configured; private IntPtr _hwnd; // URL the WebView2 is currently showing, so a re-summon re-navigates only // when the model id actually changed (preserves an in-flight chat). Null // until the first navigation lands. private Uri? _currentUri; // Suppress the spurious deactivation that races ahead of (or lands just // after) the show sequence when Windows denies us foreground — without it // the overlay hides itself straight back, looking like it never opened. private const long ShownDeactivationGraceMs = 250; private bool _suppressDeactivate; private long _lastShownMs; public OverlayWindow() { InitializeComponent(); ConfigureAsOverlay(); Activated += OverlayWindow_Activated; } /// Style as a borderless, taskbar-less spotlight popup centered on screen. private void ConfigureAsOverlay() { if (_configured) return; _configured = true; var presenter = (OverlappedPresenter)AppWindow.Presenter; presenter.SetBorderAndTitleBar(false, false); presenter.IsResizable = false; presenter.IsMaximizable = false; presenter.IsMinimizable = false; AppWindow.IsShownInSwitchers = false; // keep out of Alt-Tab / taskbar _hwnd = WindowNative.GetWindowHandle(this); // WS_EX_TOOLWINDOW: no taskbar entry. WS_POPUP: drop the 1px frame so // the Mica surface is flush to the rounded corners (same trick the // tray flyout uses). SetWindowLongCompat(_hwnd, GWL_EXSTYLE, (IntPtr)(GetWindowLongCompat(_hwnd, GWL_EXSTYLE).ToInt32() | WS_EX_TOOLWINDOW)); SetWindowLongCompat(_hwnd, GWL_STYLE, new IntPtr(WS_POPUP)); const uint SWP_NOMOVE = 0x0002, SWP_NOSIZE = 0x0001, SWP_NOZORDER = 0x0004, SWP_NOOWNERZORDER = 0x0200, SWP_FRAMECHANGED = 0x0020; SetWindowPos(_hwnd, IntPtr.Zero, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED); // Pin the corner radius to the standard 8px "round" style rather than // relying on the system default. WindowCorners.ApplyRound8(this); } /// Shows the overlay centered on the monitor nearest the cursor, ready for input. public void Summon() { // Show the window FIRST, then populate content. Mutating a batch of // control properties on a frameless WS_POPUP Mica window before it has // been shown can trigger a layout pass on an un-shown window that faults // in the native WinUI layer (0xC0000005). The original working version // set only InputBox.IsEnabled before Show; we restore that order and do // all content updates after the window is on screen. Common.Log.Info("overlay summon: begin"); CenterOnScreen(); _lastShownMs = Environment.TickCount64; _suppressDeactivate = true; AppWindow.Show(); ShowWindow(_hwnd, SW_SHOW); SetForegroundWindow(_hwnd); Common.Log.Info("overlay summon: window shown"); // Now that the window is on screen, refresh the chrome and navigate // the WebView2 to the loaded model's WebUI. RefreshModelBadge(); NavigateToModel(); Common.Log.Info("overlay summon: done"); } /// /// Builds the URL the WebView2 should show: the running llama server's /// WebUI, scoped to the currently loaded model when one is resident. This /// mirrors MainWindow.LocalModelOpen_Click's /// ?model=<ServerModelId> pattern (passing ?model= makes /// the server auto-load that model). With no model loaded we fall back to /// the bare WebUI — router mode hosts it even with no model, so the user /// can browse/download models from inside the overlay too. /// private static Uri BuildModelUri() { var baseUrl = $"http://localhost:{LlamaManager.Shared.ServerPort}"; var id = LlamaManager.Shared.LoadedModelId; return id is null ? new Uri(baseUrl) : new Uri($"{baseUrl}?model={Uri.EscapeDataString(id)}"); } /// /// Points the embedded WebView2 at . The /// initial Source assignment also lazily initializes the WebView2's /// CoreWebView2 (setting Source is equivalent to EnsureCoreWebView2Async /// + Navigate). Only re-navigates when the URL changes so a re-summon /// mid-chat doesn't reset it. /// private void NavigateToModel() { var uri = BuildModelUri(); if (_currentUri is not null && _currentUri == uri) { Common.Log.Info($"overlay webview: URL unchanged ({uri}), keeping current page"); return; } _currentUri = uri; ModelWebUi.Source = uri; Common.Log.Info($"overlay webview: navigating to {uri}"); } private void RefreshModelBadge() { var id = LlamaManager.Shared.LoadedModelId; var loaded = id is not null; ModelBadge.Text = loaded ? id! : "No model"; StatusDot.Fill = loaded ? new SolidColorBrush(Color.FromArgb(0xFF, 0x3F, 0xB9, 0x50)) // green : new SolidColorBrush(Color.FromArgb(0xFF, 0xF5, 0xA6, 0x23)); // amber } private void CenterOnScreen() { var area = GetCursorMonitorWorkArea(); // The fraction of the work area is already in physical pixels (like // area itself); only the DIPs minimum needs scaling to this monitor's // DPI before the two can be compared. var scale = GetCursorMonitorDpiScale(); var minW = (int)Math.Round(OverlayMinWidthDips * scale); var minH = (int)Math.Round(OverlayMinHeightDips * scale); var w = Math.Max(minW, (int)(area.Width * OverlayWidthFraction)); var h = Math.Max(minH, (int)(area.Height * OverlayHeightFraction)); AppWindow.Resize(new SizeInt32(w, h)); var x = area.X + (area.Width - w) / 2; var y = area.Y + (area.Height - h) / 2; AppWindow.Move(new PointInt32(x, y)); } /// /// DPI scale (1.0 = 100%) of the monitor nearest the cursor — the monitor /// targets. Defaults to 1.0 if the query fails. /// private static double GetCursorMonitorDpiScale() { GetCursorPos(out var p); var mon = MonitorFromPoint(p, MONITOR_DEFAULTTONEAREST); return GetDpiForMonitor(mon, MDT_EFFECTIVE_DPI, out var dpiX, out _) == 0 ? dpiX / 96.0 : 1.0; } private RectInt32 GetCursorMonitorWorkArea() { // Place near the cursor's monitor so the spotlight follows the user // across multi-monitor setups (like macOS spotlight on the active space). GetCursorPos(out var p); var mon = MonitorFromPoint(p, MONITOR_DEFAULTTONEAREST); var info = new MONITORINFO { cbSize = (uint)Marshal.SizeOf() }; GetMonitorInfoW(mon, ref info); return new RectInt32(info.rcWork.Left, info.rcWork.Top, info.rcWork.Right - info.rcWork.Left, info.rcWork.Bottom - info.rcWork.Top); } /// Esc accelerator (bound in XAML) dismisses the overlay. private void EscapeAccelerator_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) { Hide(); args.Handled = true; } private void OverlayWindow_Activated(object sender, WindowActivatedEventArgs args) { // Dismiss on deactivation (clicking away), the same UX as the tray flyout. if (args.WindowActivationState == WindowActivationState.Deactivated) { if (_suppressDeactivate || Environment.TickCount64 - _lastShownMs <= ShownDeactivationGraceMs) { _suppressDeactivate = false; return; } Hide(); } else { _suppressDeactivate = false; } } private void Hide() { AppWindow.Hide(); ShowWindow(_hwnd, SW_HIDE); } // ---- Win32 ---- private const int SW_SHOW = 5, SW_HIDE = 0; private const uint MONITOR_DEFAULTTONEAREST = 0x00000002; [StructLayout(LayoutKind.Sequential)] private struct POINT { public int X; public int Y; } [StructLayout(LayoutKind.Sequential)] private struct RECT { public int Left, Top, Right, Bottom; } [StructLayout(LayoutKind.Sequential)] private struct MONITORINFO { public uint cbSize; public RECT rcMonitor; public RECT rcWork; public uint dwFlags; } [DllImport("user32.dll")] private static extern bool GetCursorPos(out POINT lpPoint); [DllImport("user32.dll")] private static extern IntPtr MonitorFromPoint(POINT pt, uint dwFlags); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern bool GetMonitorInfoW(IntPtr hMonitor, ref MONITORINFO lpmi); [DllImport("shcore.dll")] private static extern int GetDpiForMonitor(IntPtr hmonitor, int dpiType, out uint dpiX, out uint dpiY); private const int MDT_EFFECTIVE_DPI = 0; [DllImport("user32.dll", EntryPoint = "GetWindowLong")] private static extern int GetWindowLong32(IntPtr hwnd, int nIndex); [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")] private static extern IntPtr GetWindowLongPtr64(IntPtr hwnd, int nIndex); [DllImport("user32.dll", EntryPoint = "SetWindowLong")] private static extern int SetWindowLong32(IntPtr hwnd, int nIndex, int value); [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")] private static extern IntPtr SetWindowLongPtr64(IntPtr hwnd, int nIndex, IntPtr value); private static IntPtr GetWindowLongCompat(IntPtr hwnd, int nIndex) => IntPtr.Size == 4 ? (IntPtr)GetWindowLong32(hwnd, nIndex) : GetWindowLongPtr64(hwnd, nIndex); private static void SetWindowLongCompat(IntPtr hwnd, int nIndex, IntPtr value) { if (IntPtr.Size == 4) SetWindowLong32(hwnd, nIndex, value.ToInt32()); else SetWindowLongPtr64(hwnd, nIndex, value); } [DllImport("user32.dll")] private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll")] private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); }