This commit is contained in:
@@ -5,7 +5,7 @@ using CursorLang.Core.Interop;
|
||||
namespace CursorLang.Agent.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Plain GDI: a font, a brush, a rounded region and text on a device context.
|
||||
/// Plain GDI: a font, text, and an off-screen bitmap to draw them into.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// GDI rather than GDI+ on purpose. <c>System.Drawing.Common</c> would make the drawing
|
||||
@@ -85,11 +85,41 @@ internal static class GdiNative
|
||||
int iCharSet, int iOutPrecision, int iClipPrecision, int iQuality, int iPitchAndFamily,
|
||||
string pszFaceName);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
internal static extern IntPtr CreateSolidBrush(uint color);
|
||||
/// <summary>
|
||||
/// A 32-bit surface to draw the popup into before anyone can see it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Top-down — a negative height — so that the first row of <paramref name="bits"/>
|
||||
/// is the top row of the picture and the alpha fixing up afterwards can walk the
|
||||
/// memory straight through.
|
||||
/// </remarks>
|
||||
internal static IntPtr CreateSurface(IntPtr deviceContext, int width, int height, out IntPtr bits)
|
||||
{
|
||||
var header = new BitmapInfoHeader
|
||||
{
|
||||
biSize = Marshal.SizeOf<BitmapInfoHeader>(),
|
||||
biWidth = width,
|
||||
biHeight = -height,
|
||||
biPlanes = 1,
|
||||
biBitCount = 32,
|
||||
biCompression = BI_RGB,
|
||||
};
|
||||
|
||||
return CreateDIBSection(deviceContext, ref header, DIB_RGB_COLORS, out bits, IntPtr.Zero, 0);
|
||||
}
|
||||
|
||||
private const uint BI_RGB = 0;
|
||||
private const uint DIB_RGB_COLORS = 0;
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
internal static extern IntPtr CreateRoundRectRgn(int x1, int y1, int x2, int y2, int w, int h);
|
||||
internal static extern IntPtr CreateCompatibleDC(IntPtr hdc);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
internal static extern bool DeleteDC(IntPtr hdc);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
private static extern IntPtr CreateDIBSection(IntPtr hdc, ref BitmapInfoHeader header, uint usage,
|
||||
out IntPtr bits, IntPtr section, uint offset);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
internal static extern IntPtr SelectObject(IntPtr hdc, IntPtr h);
|
||||
@@ -103,9 +133,6 @@ internal static class GdiNative
|
||||
[DllImport("gdi32.dll")]
|
||||
internal static extern uint SetTextColor(IntPtr hdc, uint color);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern int FillRect(IntPtr hdc, ref PopupWindowNative.Rect lprc, IntPtr hbr);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "DrawTextW")]
|
||||
internal static extern int DrawText(IntPtr hdc, string lpchText, int cchText,
|
||||
ref PopupWindowNative.Rect lprc, uint format);
|
||||
@@ -116,22 +143,19 @@ internal static class GdiNative
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern IntPtr BeginPaint(IntPtr hWnd, out PaintStruct lpPaint);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern bool EndPaint(IntPtr hWnd, ref PaintStruct lpPaint);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct PaintStruct
|
||||
private struct BitmapInfoHeader
|
||||
{
|
||||
public IntPtr hdc;
|
||||
public bool fErase;
|
||||
public PopupWindowNative.Rect rcPaint;
|
||||
public bool fRestore;
|
||||
public bool fIncUpdate;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
|
||||
public byte[] rgbReserved;
|
||||
public int biSize;
|
||||
public int biWidth;
|
||||
public int biHeight;
|
||||
public short biPlanes;
|
||||
public short biBitCount;
|
||||
public uint biCompression;
|
||||
public uint biSizeImage;
|
||||
public int biXPelsPerMeter;
|
||||
public int biYPelsPerMeter;
|
||||
public uint biClrUsed;
|
||||
public uint biClrImportant;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ internal static class WindowNative
|
||||
internal delegate IntPtr WindowProc(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
internal const int WS_POPUP = unchecked((int)0x80000000);
|
||||
internal const int WS_DISABLED = 0x08000000;
|
||||
|
||||
internal const int WS_EX_LAYERED = 0x00080000;
|
||||
internal const int WS_EX_TOOLWINDOW = 0x00000080;
|
||||
@@ -25,19 +24,14 @@ internal static class WindowNative
|
||||
internal const int SW_SHOWNOACTIVATE = 4;
|
||||
|
||||
internal const uint WM_DESTROY = 0x0002;
|
||||
internal const uint WM_PAINT = 0x000F;
|
||||
internal const uint WM_CLOSE = 0x0010;
|
||||
internal const uint WM_QUIT = 0x0012;
|
||||
internal const uint WM_NULL = 0x0000;
|
||||
internal const uint WM_DISPLAYCHANGE = 0x007E;
|
||||
internal const uint WM_DPICHANGED = 0x02E0;
|
||||
internal const uint WM_ENDSESSION = 0x0016;
|
||||
|
||||
/// <summary>WM_APP and up belong to the application; the tray takes WM_APP + 1.</summary>
|
||||
internal const uint WM_APP = 0x8000;
|
||||
|
||||
private const uint LWA_ALPHA = 0x00000002;
|
||||
|
||||
/// <summary>
|
||||
/// Registers a window class. A class already there is not an error: the name is
|
||||
/// unique per window kind, and a second agent in the same process would meet its
|
||||
@@ -80,9 +74,55 @@ internal static class WindowNative
|
||||
return window;
|
||||
}
|
||||
|
||||
/// <summary>The whole opacity of a layered window, 0 to 255.</summary>
|
||||
internal static void SetAlpha(IntPtr window, byte alpha) =>
|
||||
SetLayeredWindowAttributes(window, 0, alpha, LWA_ALPHA);
|
||||
/// <summary>
|
||||
/// Puts a finished picture into a layered window, together with where it goes and
|
||||
/// how see-through it is.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// One call replaces moving the window, resizing it, painting it and setting its
|
||||
/// opacity, and it works while the window is still hidden. That is the point: the
|
||||
/// content is ready before anyone can see the window, so it can never be shown
|
||||
/// holding the picture of the previous time.
|
||||
/// </remarks>
|
||||
internal static bool SetContent(
|
||||
IntPtr window, PopupWindowNative.Point at, Size size, IntPtr sourceDc, byte alpha)
|
||||
{
|
||||
var source = new PopupWindowNative.Point { X = 0, Y = 0 };
|
||||
var blend = new BlendFunction
|
||||
{
|
||||
BlendOp = AC_SRC_OVER,
|
||||
SourceConstantAlpha = alpha,
|
||||
AlphaFormat = AC_SRC_ALPHA,
|
||||
};
|
||||
|
||||
return UpdateLayeredWindow(
|
||||
window, IntPtr.Zero, ref at, ref size, sourceDc, ref source, 0, ref blend, ULW_ALPHA);
|
||||
}
|
||||
|
||||
private const byte AC_SRC_OVER = 0;
|
||||
private const byte AC_SRC_ALPHA = 1;
|
||||
private const uint ULW_ALPHA = 0x00000002;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct BlendFunction
|
||||
{
|
||||
public byte BlendOp;
|
||||
public byte BlendFlags;
|
||||
public byte SourceConstantAlpha;
|
||||
public byte AlphaFormat;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Size
|
||||
{
|
||||
public int Width;
|
||||
public int Height;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool UpdateLayeredWindow(IntPtr hWnd, IntPtr hdcDst,
|
||||
ref PopupWindowNative.Point pptDst, ref Size psize, IntPtr hdcSrc,
|
||||
ref PopupWindowNative.Point pptSrc, uint crKey, ref BlendFunction pblend, uint dwFlags);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern ushort RegisterClassEx(ref WindowClass lpwcx);
|
||||
@@ -104,27 +144,12 @@ internal static class WindowNative
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern bool IsWindowVisible(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern bool InvalidateRect(IntPtr hWnd, IntPtr lpRect, bool bErase);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern bool UpdateWindow(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern bool SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern bool GetClientRect(IntPtr hWnd, out PopupWindowNative.Rect lpRect);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "PostMessageW")]
|
||||
internal static extern bool PostMessage(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern void PostQuitMessage(int nExitCode);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool SetLayeredWindowAttributes(IntPtr hWnd, uint crKey, byte bAlpha, uint dwFlags);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern IntPtr GetModuleHandle(string? lpModuleName);
|
||||
|
||||
|
||||
@@ -115,9 +115,28 @@ public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnHoldTimerTick(object? sender, EventArgs e)
|
||||
private void OnHoldTimerTick(object? sender, EventArgs e) => HandleHoldElapsed();
|
||||
|
||||
/// <summary>
|
||||
/// The hold countdown has run out.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The key being down is checked rather than assumed. A countdown started on the
|
||||
/// press can still be delivered just after the release — Windows does not withdraw a
|
||||
/// WM_TIMER it has already posted — and announcing a hold then would put the popup on
|
||||
/// screen showing the layout the tap is about to change away from.
|
||||
///
|
||||
/// The tests reach this directly: that ordering is the whole point and a real clock
|
||||
/// will not reproduce it on demand.
|
||||
/// </remarks>
|
||||
internal void HandleHoldElapsed()
|
||||
{
|
||||
_holdTimer.Stop();
|
||||
if (!_isPressed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isHolding = true;
|
||||
HoldStarted?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.InteropServices;
|
||||
using CursorLang.Agent.Interop;
|
||||
using CursorLang.Agent.Services;
|
||||
using CursorLang.Core.Interop;
|
||||
@@ -14,15 +15,18 @@ namespace CursorLang.Agent.Windows;
|
||||
/// 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.
|
||||
/// from the settings.
|
||||
///
|
||||
/// The opacity comes from <c>SetLayeredWindowAttributes</c> 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.
|
||||
/// The picture is drawn into an off-screen bitmap and handed to the window whole, by
|
||||
/// <c>UpdateLayeredWindow</c>. Painting on demand instead — a <c>WM_PAINT</c> 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
|
||||
/// <c>SetLayeredWindowAttributes</c>. 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 <see cref="LayoutPopupService"/>.
|
||||
@@ -38,16 +42,9 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
||||
|
||||
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(
|
||||
@@ -71,8 +68,6 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
||||
return;
|
||||
}
|
||||
|
||||
_text = text;
|
||||
|
||||
bool atFixedPoint = _settings.PlacementMode == PopupPlacementMode.FixedPoint;
|
||||
PopupWindowNative.Rect work = default;
|
||||
PopupWindowNative.Rect anchor = default;
|
||||
@@ -90,7 +85,7 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
||||
|
||||
EnsureFont(scale);
|
||||
|
||||
Size measured = MeasureText();
|
||||
Size measured = MeasureText(text);
|
||||
int width = measured.Width + (2 * PopupLayout.ToPixels(PaddingX, scale));
|
||||
int height = measured.Height + (2 * PopupLayout.ToPixels(PaddingY, scale));
|
||||
|
||||
@@ -99,21 +94,15 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
||||
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 (!Draw(text, position, width, height, PopupLayout.ToPixels(CornerRadius, scale)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -130,27 +119,205 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
||||
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)
|
||||
/// <summary>
|
||||
/// Draws the popup off screen and hands the finished picture to the window.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
private bool Draw(string text, PopupWindowNative.Point at, int width, int height, int radius)
|
||||
{
|
||||
result = IntPtr.Zero;
|
||||
|
||||
if (message != WindowNative.WM_PAINT)
|
||||
IntPtr screen = GdiNative.GetDC(IntPtr.Zero);
|
||||
if (screen == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Paint();
|
||||
return true;
|
||||
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(bits, width, height);
|
||||
DrawText(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(_settings.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 void Fill(IntPtr bits, int width, int height)
|
||||
{
|
||||
Color background = _settings.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(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(_settings.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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cuts the four corners to a radius, fading the edge rather than stepping it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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
|
||||
/// <c>UpdateLayeredWindow</c> expects.
|
||||
/// </remarks>
|
||||
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);
|
||||
}
|
||||
|
||||
// The anchor point: the caret in the input field or the mouse cursor. The cursor
|
||||
@@ -174,42 +341,7 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
||||
_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()
|
||||
private Size MeasureText(string text)
|
||||
{
|
||||
IntPtr deviceContext = GdiNative.GetDC(Handle);
|
||||
if (deviceContext == IntPtr.Zero)
|
||||
@@ -220,10 +352,10 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
||||
try
|
||||
{
|
||||
IntPtr previousFont = GdiNative.SelectObject(deviceContext, _font);
|
||||
Size size = GdiNative.MeasureText(deviceContext, _text);
|
||||
Size measured = GdiNative.MeasureText(deviceContext, text);
|
||||
GdiNative.SelectObject(deviceContext, previousFont);
|
||||
|
||||
return size;
|
||||
return measured;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -231,13 +363,13 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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
|
||||
private void EnsureFont(double scale)
|
||||
{
|
||||
if (_font != IntPtr.Zero &&
|
||||
Math.Abs(_fontSize - _settings.FontSize) < 0.001 &&
|
||||
Math.Abs(_fontScale - scale) < 0.001)
|
||||
Math.Abs(_fontSize - _settings.FontSize) < 0.01 &&
|
||||
Math.Abs(_fontScale - scale) < 0.01)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -251,71 +383,10 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
||||
|
||||
private void ReleaseFont()
|
||||
{
|
||||
if (_font == IntPtr.Zero)
|
||||
if (_font != IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
GdiNative.DeleteObject(_font);
|
||||
_font = IntPtr.Zero;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,9 +67,14 @@ internal abstract class NativeWindow : IDisposable
|
||||
|
||||
/// <summary>
|
||||
/// A message for this window. Returning <c>false</c> passes it to
|
||||
/// <c>DefWindowProcW</c>, which is what the vast majority of messages want.
|
||||
/// <c>DefWindowProcW</c>, which is what the vast majority of messages want — and
|
||||
/// what all of them want for a window whose content is set from the outside.
|
||||
/// </summary>
|
||||
protected abstract bool OnMessage(uint message, IntPtr wParam, IntPtr lParam, out IntPtr result);
|
||||
protected virtual bool OnMessage(uint message, IntPtr wParam, IntPtr lParam, out IntPtr result)
|
||||
{
|
||||
result = IntPtr.Zero;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IntPtr StaticWindowProc(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user