using System.Drawing; using CursorLang.Agent.Interop; using CursorLang.Agent.Services; using CursorLang.Core.Interop; using CursorLang.Core.Models; using CursorLang.Core.Services; namespace CursorLang.Agent.Windows; /// /// The popup with the short name of the layout, drawn by Win32 alone. /// /// /// A like-for-like replacement of the WPF popup this once was: a rounded rectangle of /// radius 4 with 10×4 padding, the fill and the text colour from the settings, the whole /// thing at the opacity from the settings, the name in Segoe UI SemiBold at the size /// from the settings. Measured against it side by side, the two agreed to the pixel in /// position and width. /// /// The opacity comes from SetLayeredWindowAttributes and the rounded corners from /// a window region, which is the cheaper of the two ways of doing it: the text keeps /// ClearType, and no GDI+ or Direct2D is needed. The price is that a region is a binary /// mask — the corners are cut without antialiasing. At radius 4 that is a stepped arc of /// some fourteen pixels per corner, visible at eight times magnification and not at /// natural size, which is why this way was kept. /// /// Responsible only for showing the popup, its size and its place on screen: when to /// take it down is decided by . /// internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow { private const string ClassName = "CursorLang.Agent.Popup"; // The numbers of the XAML: Border CornerRadius="4" Padding="10,4", all in WPF units private const double CornerRadius = 4; private const double PaddingX = 10; private const double PaddingY = 4; private readonly AppSettings _settings; private string _text = string.Empty; private IntPtr _font; private IntPtr _background; private double _fontSize; private double _fontScale; private uint _backgroundColorRef; private byte? _alpha; private Size _regionSize; private int _regionRadius = -1; internal NativePopupWindow(AppSettings settings) : base( ClassName, "CursorLang popup", WindowNative.WS_POPUP, WindowNative.WS_EX_LAYERED | WindowNative.WS_EX_TOOLWINDOW | WindowNative.WS_EX_NOACTIVATE | WindowNative.WS_EX_TRANSPARENT | WindowNative.WS_EX_TOPMOST) { _settings = settings; } /// /// Shows the popup with the given text at the place set by the settings. /// public void ShowPopup(string text) { if (Handle == IntPtr.Zero) { return; } _text = text; bool atFixedPoint = _settings.PlacementMode == PopupPlacementMode.FixedPoint; PopupWindowNative.Rect work = default; PopupWindowNative.Rect anchor = default; double scale; if (atFixedPoint) { (work, scale) = PopupWindowNative.GetActiveMonitorWorkArea(); } else { anchor = GetAnchor(); scale = PopupWindowNative.GetScaleAt(new PopupWindowNative.Point { X = anchor.Left, Y = anchor.Top }); } EnsureFont(scale); Size measured = MeasureText(); int width = measured.Width + (2 * PopupLayout.ToPixels(PaddingX, scale)); int height = measured.Height + (2 * PopupLayout.ToPixels(PaddingY, scale)); PopupWindowNative.Point position = atFixedPoint ? PopupLayout.OnScreen( work, _settings.ScreenPosition, PopupLayout.ToPixels(_settings.ScreenMargin, scale), width, height) : PopupLayout.NearAnchor(anchor, AnchorSideForMode(), OffsetForMode(scale), width, height); PopupWindowNative.SetBounds(Handle, position.X, position.Y, width, height); ApplyRegion(width, height, PopupLayout.ToPixels(CornerRadius, scale)); ApplyOpacity(); if (!WindowNative.IsWindowVisible(Handle)) { WindowNative.ShowWindow(Handle, WindowNative.SW_SHOWNOACTIVATE); } // The repaint is forced rather than left to the queue: a layout switch is // followed at once by the user looking at the popup, and a WM_PAINT waiting its // turn behind a slow message is exactly how stale text gets on screen WindowNative.InvalidateRect(Handle, IntPtr.Zero, false); WindowNative.UpdateWindow(Handle); } public void Hide() { if (Handle != IntPtr.Zero) { WindowNative.ShowWindow(Handle, WindowNative.SW_HIDE); } } /// Destroys the window. The agent only does this on the way out. public void Close() => Dispose(); public override void Dispose() { ReleaseFont(); if (_background != IntPtr.Zero) { GdiNative.DeleteObject(_background); _background = IntPtr.Zero; } base.Dispose(); } protected override bool OnMessage(uint message, IntPtr wParam, IntPtr lParam, out IntPtr result) { result = IntPtr.Zero; if (message != WindowNative.WM_PAINT) { return false; } Paint(); return true; } // The anchor point: the caret in the input field or the mouse cursor. The cursor // is a rectangle of zero size, so the corner computation is shared by both private PopupWindowNative.Rect GetAnchor() { if (_settings.PlacementMode == PopupPlacementMode.AtCaret && CaretNative.TryGetCaretRect() is { } caret) { return caret; } return PopupLayout.AsAnchor(PopupWindowNative.GetCursorPosition()); } // Every anchor mode has a side and an offset of its own private AnchorSide AnchorSideForMode() => _settings.PlacementMode == PopupPlacementMode.AtCaret ? _settings.CaretSide : _settings.CursorSide; private int OffsetForMode(double scale) => PopupLayout.ToPixels( _settings.PlacementMode == PopupPlacementMode.AtCaret ? _settings.CaretOffset : _settings.CursorOffset, scale); private void Paint() { IntPtr deviceContext = GdiNative.BeginPaint(Handle, out GdiNative.PaintStruct paint); if (deviceContext == IntPtr.Zero) { return; } try { WindowNative.GetClientRect(Handle, out PopupWindowNative.Rect client); GdiNative.FillRect(deviceContext, ref client, EnsureBackground()); if (_font == IntPtr.Zero || _text.Length == 0) { return; } IntPtr previousFont = GdiNative.SelectObject(deviceContext, _font); GdiNative.SetBkMode(deviceContext, GdiNative.TRANSPARENT); GdiNative.SetTextColor(deviceContext, GdiNative.ToColorRef(_settings.ForegroundColor)); GdiNative.DrawText(deviceContext, _text, _text.Length, ref client, GdiNative.DT_SINGLELINE | GdiNative.DT_CENTER | GdiNative.DT_VCENTER | GdiNative.DT_NOPREFIX | GdiNative.DT_NOCLIP); GdiNative.SelectObject(deviceContext, previousFont); } finally { GdiNative.EndPaint(Handle, ref paint); } } private Size MeasureText() { IntPtr deviceContext = GdiNative.GetDC(Handle); if (deviceContext == IntPtr.Zero) { return Size.Empty; } try { IntPtr previousFont = GdiNative.SelectObject(deviceContext, _font); Size size = GdiNative.MeasureText(deviceContext, _text); GdiNative.SelectObject(deviceContext, previousFont); return size; } finally { GdiNative.ReleaseDC(Handle, deviceContext); } } // The font is rebuilt only when the size in the settings or the monitor changes: // creating one costs a trip to the font mapper, and the popup is shown often private void EnsureFont(double scale) { if (_font != IntPtr.Zero && Math.Abs(_fontSize - _settings.FontSize) < 0.001 && Math.Abs(_fontScale - scale) < 0.001) { return; } ReleaseFont(); _fontSize = _settings.FontSize; _fontScale = scale; _font = GdiNative.CreateFont(_fontSize, scale); } private void ReleaseFont() { if (_font == IntPtr.Zero) { return; } GdiNative.DeleteObject(_font); _font = IntPtr.Zero; } private IntPtr EnsureBackground() { uint colorRef = GdiNative.ToColorRef(_settings.BackgroundColor); if (_background != IntPtr.Zero && _backgroundColorRef == colorRef) { return _background; } if (_background != IntPtr.Zero) { GdiNative.DeleteObject(_background); } _backgroundColorRef = colorRef; _background = GdiNative.CreateSolidBrush(colorRef); return _background; } // A region is in window coordinates, so a resize invalidates it. Reapplying it // every show would do no harm, but the system frees the old region each time and // the popup is shown far more often than it changes size private void ApplyRegion(int width, int height, int radius) { if (_regionSize.Width == width && _regionSize.Height == height && _regionRadius == radius) { return; } // The right and bottom edges of CreateRoundRectRgn are exclusive, and the // ellipse the corners are cut with is twice the radius across IntPtr region = GdiNative.CreateRoundRectRgn(0, 0, width + 1, height + 1, radius * 2, radius * 2); if (region == IntPtr.Zero) { return; } if (!WindowNative.SetWindowRgn(Handle, region, false)) { GdiNative.DeleteObject(region); return; } _regionSize = new Size(width, height); _regionRadius = radius; } private void ApplyOpacity() { var alpha = (byte)Math.Clamp(Math.Round(_settings.Opacity * 255), 0, 255); if (alpha == _alpha) { return; } _alpha = alpha; WindowNative.SetAlpha(Handle, alpha); } }