using System.Drawing; using System.Runtime.InteropServices; 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. /// /// The picture is drawn into an off-screen bitmap and handed to the window whole, by /// UpdateLayeredWindow. Painting on demand instead — a WM_PAINT after the /// window is shown — is what the first version did, and it had the popup appear holding /// the picture of the previous show: hiding a window does not throw its content away, /// and the content is always the other layout. Here there is nothing to be stale, /// because the window is never shown before its picture is in place. /// /// It also does away with two devices the painted version needed: the corners came from /// a window region, which cuts without antialiasing, and the opacity from /// SetLayeredWindowAttributes. Both are now just pixels in the bitmap. /// /// 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 IntPtr _font; private double _fontSize; private double _fontScale; 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; } PopupWindowNative.Rect? anchor = TryGetAnchor(); PopupModeSettings mode = ModeFor(anchor); PopupWindowNative.Rect work = default; double scale; if (anchor is { } at) { scale = PopupWindowNative.GetScaleAt(new PopupWindowNative.Point { X = at.Left, Y = at.Top }); } else { (work, scale) = PopupWindowNative.GetActiveMonitorWorkArea(); } EnsureFont(mode, scale); Size measured = MeasureText(text); int width = measured.Width + (2 * PopupLayout.ToPixels(PaddingX, scale)); int height = measured.Height + (2 * PopupLayout.ToPixels(PaddingY, scale)); PopupWindowNative.Point position = anchor is { } near ? PopupLayout.NearAnchor( near, SideForMode(), PopupLayout.ToPixels(mode.Offset, scale), width, height) : PopupLayout.OnScreen( work, _settings.FixedPoint.Position, PopupLayout.ToPixels(mode.Offset, scale), width, height); if (!Draw(mode, text, position, width, height, PopupLayout.ToPixels(CornerRadius, scale))) { return; } if (!WindowNative.IsWindowVisible(Handle)) { WindowNative.ShowWindow(Handle, WindowNative.SW_SHOWNOACTIVATE); } } 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(); base.Dispose(); } /// /// Draws the popup off screen and hands the finished picture to the window. /// /// /// The bitmap is thrown away afterwards rather than kept: it is a few tens of /// kilobytes for the length of one call, the popup is shown rarely, and a cached one /// would have to be rebuilt on every change of size, colour or scale anyway. /// private bool Draw( PopupModeSettings mode, string text, PopupWindowNative.Point at, int width, int height, int radius) { IntPtr screen = GdiNative.GetDC(IntPtr.Zero); if (screen == IntPtr.Zero) { return false; } IntPtr memory = IntPtr.Zero; IntPtr surface = IntPtr.Zero; try { memory = GdiNative.CreateCompatibleDC(screen); if (memory == IntPtr.Zero) { return false; } surface = GdiNative.CreateSurface(memory, width, height, out IntPtr bits); if (surface == IntPtr.Zero) { return false; } GdiNative.SelectObject(memory, surface); Fill(mode, bits, width, height); DrawText(mode, memory, text, width, height); // GDI writes nothing into the alpha channel, so the letters it just drew are // sitting at zero alpha and would come out invisible. The inside of the // popup is opaque anyway, so the whole surface is simply declared so — and // the corners are rounded off afterwards, which is the only place alpha // varies MakeOpaque(bits, width, height); RoundTheCorners(bits, width, height, radius); var size = new WindowNative.Size { Width = width, Height = height }; var alpha = (byte)Math.Clamp(Math.Round(mode.Opacity * 255), 0, 255); return WindowNative.SetContent(Handle, at, size, memory, alpha); } finally { // The context goes first: a bitmap still selected into one cannot be // deleted, and this way that holds however the method was left if (memory != IntPtr.Zero) { GdiNative.DeleteDC(memory); } if (surface != IntPtr.Zero) { GdiNative.DeleteObject(surface); } GdiNative.ReleaseDC(IntPtr.Zero, screen); } } private static void Fill(PopupModeSettings mode, IntPtr bits, int width, int height) { Color background = mode.BackgroundColor; // Straight into the bitmap rather than through a brush: the pixels have to be // written anyway to carry an alpha channel GDI would not touch int packed = (255 << 24) | (background.R << 16) | (background.G << 8) | background.B; var row = new int[width]; Array.Fill(row, packed); for (var y = 0; y < height; y++) { Marshal.Copy(row, 0, bits + (y * width * 4), width); } } private void DrawText( PopupModeSettings mode, IntPtr deviceContext, string text, int width, int height) { if (_font == IntPtr.Zero || text.Length == 0) { return; } var bounds = new PopupWindowNative.Rect { Left = 0, Top = 0, Right = width, Bottom = height }; IntPtr previousFont = GdiNative.SelectObject(deviceContext, _font); GdiNative.SetBkMode(deviceContext, GdiNative.TRANSPARENT); GdiNative.SetTextColor(deviceContext, GdiNative.ToColorRef(mode.ForegroundColor)); GdiNative.DrawText(deviceContext, text, text.Length, ref bounds, GdiNative.DT_SINGLELINE | GdiNative.DT_CENTER | GdiNative.DT_VCENTER | GdiNative.DT_NOPREFIX | GdiNative.DT_NOCLIP); GdiNative.SelectObject(deviceContext, previousFont); } private static void MakeOpaque(IntPtr bits, int width, int height) { var row = new int[width]; for (var y = 0; y < height; y++) { IntPtr line = bits + (y * width * 4); Marshal.Copy(line, row, 0, width); for (var x = 0; x < width; x++) { row[x] = (int)((uint)row[x] | 0xFF000000); } Marshal.Copy(row, 0, line, width); } } /// /// Cuts the four corners to a radius, fading the edge rather than stepping it. /// /// /// The painted version cut them with a window region, which is a yes-or-no mask and /// left a visible staircase at 200% scale. Here the corner pixels carry a partial /// alpha worked out from how far the pixel centre is past the arc, which is what /// antialiasing amounts to. The colours are premultiplied to match, as /// UpdateLayeredWindow expects. /// private static void RoundTheCorners(IntPtr bits, int width, int height, int radius) { if (radius <= 0) { return; } radius = Math.Min(radius, Math.Min(width, height) / 2); var row = new int[width]; for (var y = 0; y < height; y++) { bool nearTop = y < radius; bool nearBottom = y >= height - radius; if (!nearTop && !nearBottom) { continue; } IntPtr line = bits + (y * width * 4); Marshal.Copy(line, row, 0, width); double centreY = nearTop ? radius - 0.5 : height - radius - 0.5; for (var x = 0; x < width; x++) { bool nearLeft = x < radius; bool nearRight = x >= width - radius; if (!nearLeft && !nearRight) { continue; } double centreX = nearLeft ? radius - 0.5 : width - radius - 0.5; double distance = Math.Sqrt( ((x - centreX) * (x - centreX)) + ((y - centreY) * (y - centreY))); // One pixel of softness across the arc: fully inside, fully outside, // and a ramp in between double coverage = Math.Clamp(radius - distance + 0.5, 0, 1); if (coverage >= 1) { continue; } row[x] = Premultiply(row[x], coverage); } Marshal.Copy(row, 0, line, width); } } private static int Premultiply(int pixel, double coverage) { var value = (uint)pixel; var alpha = (uint)Math.Round(((value >> 24) & 0xFF) * coverage); uint red = (uint)Math.Round(((value >> 16) & 0xFF) * coverage); uint green = (uint)Math.Round(((value >> 8) & 0xFF) * coverage); uint blue = (uint)Math.Round((value & 0xFF) * coverage); return (int)((alpha << 24) | (red << 16) | (green << 8) | blue); } /// /// What the popup is placed next to, or null when there is nothing: the fixed /// point mode, and the caret mode where the application reports no caret. /// /// /// The cursor is a rectangle of zero size, so the corner arithmetic is shared by it /// and the caret. /// private PopupWindowNative.Rect? TryGetAnchor() => _settings.PlacementMode switch { PopupPlacementMode.FixedPoint => null, PopupPlacementMode.AtCaret => CaretNative.TryGetCaretRect(), _ => PopupLayout.AsAnchor(PopupWindowNative.GetCursorPosition()), }; /// /// The settings the popup is shown with: those of the mode chosen, or those of the /// fixed point when there is no anchor to stand next to. /// /// /// The caret mode falls back to the fixed point rather than to the mouse cursor: /// the cursor is wherever it was last left — off to a side, on another monitor, or /// over the very text being typed — and a popup that lands there while the eyes are /// on the caret is one that is looked for and not found. The fixed point is always /// in the same place, so it is known where to look. /// /// The look comes from the fixed point mode too, not just the place. The two are set /// up together for a reason: the popup by the caret is small and quiet because it /// sits inside a text being read, while the one in the corner of the monitor is /// looked for on purpose and is set larger. Keeping the caret look at the corner /// would put a popup meant to go unnoticed where nothing else draws the eye. /// private PopupModeSettings ModeFor(PopupWindowNative.Rect? anchor) => anchor is null ? _settings.FixedPoint : _settings.Current; // The caret has two sides to choose from and the cursor has six, so each mode names // its own side in its own terms private AnchorSide SideForMode() => _settings.PlacementMode == PopupPlacementMode.AtCaret ? _settings.AtCaret.Anchor : _settings.AtCursor.Side; private Size MeasureText(string text) { IntPtr deviceContext = GdiNative.GetDC(Handle); if (deviceContext == IntPtr.Zero) { return Size.Empty; } try { IntPtr previousFont = GdiNative.SelectObject(deviceContext, _font); Size measured = GdiNative.MeasureText(deviceContext, text); GdiNative.SelectObject(deviceContext, previousFont); return measured; } finally { GdiNative.ReleaseDC(Handle, deviceContext); } } // The font is rebuilt only when the size in the settings or the monitor scale // changes: it is the one expensive thing a show does. A switch of the placement // mode counts as a change of the size, since the size belongs to the mode private void EnsureFont(PopupModeSettings mode, double scale) { if (_font != IntPtr.Zero && Math.Abs(_fontSize - mode.FontSize) < 0.01 && Math.Abs(_fontScale - scale) < 0.01) { return; } ReleaseFont(); _fontSize = mode.FontSize; _fontScale = scale; _font = GdiNative.CreateFont(_fontSize, scale); } private void ReleaseFont() { if (_font != IntPtr.Zero) { GdiNative.DeleteObject(_font); _font = IntPtr.Zero; } } }