modified caret mode (#11)

Reviewed-on: #11
Co-authored-by: Aleksandr Neychev <alexnejchev73@gmail.com>
This commit was merged in pull request #11.
This commit is contained in:
2026-08-14 23:07:20 +00:00
committed by alex
parent b2c7b31eb5
commit 9c6ec489e1
5 changed files with 84 additions and 41 deletions
+62 -38
View File
@@ -68,42 +68,42 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
return;
}
bool atFixedPoint = _settings.PlacementMode == PopupPlacementMode.FixedPoint;
PopupWindowNative.Rect? anchor = TryGetAnchor();
PopupModeSettings mode = ModeFor(anchor);
PopupWindowNative.Rect work = default;
PopupWindowNative.Rect anchor = default;
double scale;
if (atFixedPoint)
if (anchor is { } at)
{
(work, scale) = PopupWindowNative.GetActiveMonitorWorkArea();
scale = PopupWindowNative.GetScaleAt(new PopupWindowNative.Point { X = at.Left, Y = at.Top });
}
else
{
anchor = GetAnchor();
scale = PopupWindowNative.GetScaleAt(new PopupWindowNative.Point { X = anchor.Left, Y = anchor.Top });
(work, scale) = PopupWindowNative.GetActiveMonitorWorkArea();
}
EnsureFont(scale);
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 = atFixedPoint
? PopupLayout.OnScreen(
work,
_settings.FixedPoint.Position,
PopupLayout.ToPixels(_settings.FixedPoint.Offset, scale),
PopupWindowNative.Point position = anchor is { } near
? PopupLayout.NearAnchor(
near,
SideForMode(),
PopupLayout.ToPixels(mode.Offset, scale),
width,
height)
: PopupLayout.NearAnchor(
anchor,
SideForMode(),
PopupLayout.ToPixels(_settings.Current.Offset, scale),
: PopupLayout.OnScreen(
work,
_settings.FixedPoint.Position,
PopupLayout.ToPixels(mode.Offset, scale),
width,
height);
if (!Draw(text, position, width, height, PopupLayout.ToPixels(CornerRadius, scale)))
if (!Draw(mode, text, position, width, height, PopupLayout.ToPixels(CornerRadius, scale)))
{
return;
}
@@ -139,7 +139,8 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
/// 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)
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)
@@ -166,8 +167,8 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
GdiNative.SelectObject(memory, surface);
Fill(bits, width, height);
DrawText(memory, text, width, height);
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
@@ -178,7 +179,7 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
RoundTheCorners(bits, width, height, radius);
var size = new WindowNative.Size { Width = width, Height = height };
var alpha = (byte)Math.Clamp(Math.Round(_settings.Current.Opacity * 255), 0, 255);
var alpha = (byte)Math.Clamp(Math.Round(mode.Opacity * 255), 0, 255);
return WindowNative.SetContent(Handle, at, size, memory, alpha);
}
@@ -200,9 +201,9 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
}
}
private void Fill(IntPtr bits, int width, int height)
private static void Fill(PopupModeSettings mode, IntPtr bits, int width, int height)
{
Color background = _settings.Current.BackgroundColor;
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
@@ -216,7 +217,8 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
}
}
private void DrawText(IntPtr deviceContext, string text, int width, int height)
private void DrawText(
PopupModeSettings mode, IntPtr deviceContext, string text, int width, int height)
{
if (_font == IntPtr.Zero || text.Length == 0)
{
@@ -227,7 +229,7 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
IntPtr previousFont = GdiNative.SelectObject(deviceContext, _font);
GdiNative.SetBkMode(deviceContext, GdiNative.TRANSPARENT);
GdiNative.SetTextColor(deviceContext, GdiNative.ToColorRef(_settings.Current.ForegroundColor));
GdiNative.SetTextColor(deviceContext, GdiNative.ToColorRef(mode.ForegroundColor));
GdiNative.DrawText(deviceContext, text, text.Length, ref bounds,
GdiNative.DT_SINGLELINE | GdiNative.DT_CENTER | GdiNative.DT_VCENTER |
@@ -329,18 +331,40 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
return (int)((alpha << 24) | (red << 16) | (green << 8) | blue);
}
// 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()
/// <summary>
/// What the popup is placed next to, or <c>null</c> when there is nothing: the fixed
/// point mode, and the caret mode where the application reports no caret.
/// </summary>
/// <remarks>
/// The cursor is a rectangle of zero size, so the corner arithmetic is shared by it
/// and the caret.
/// </remarks>
private PopupWindowNative.Rect? TryGetAnchor() => _settings.PlacementMode switch
{
if (_settings.PlacementMode == PopupPlacementMode.AtCaret &&
CaretNative.TryGetCaretRect() is { } caret)
{
return caret;
}
PopupPlacementMode.FixedPoint => null,
PopupPlacementMode.AtCaret => CaretNative.TryGetCaretRect(),
_ => PopupLayout.AsAnchor(PopupWindowNative.GetCursorPosition()),
};
return PopupLayout.AsAnchor(PopupWindowNative.GetCursorPosition());
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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
@@ -373,10 +397,10 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
// 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(double scale)
private void EnsureFont(PopupModeSettings mode, double scale)
{
if (_font != IntPtr.Zero &&
Math.Abs(_fontSize - _settings.Current.FontSize) < 0.01 &&
Math.Abs(_fontSize - mode.FontSize) < 0.01 &&
Math.Abs(_fontScale - scale) < 0.01)
{
return;
@@ -384,7 +408,7 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
ReleaseFont();
_fontSize = _settings.Current.FontSize;
_fontSize = mode.FontSize;
_fontScale = scale;
_font = GdiNative.CreateFont(_fontSize, scale);
}