diff --git a/CursorLang.Agent/Windows/NativePopupWindow.cs b/CursorLang.Agent/Windows/NativePopupWindow.cs
index 7018f43..6572f7b 100644
--- a/CursorLang.Agent/Windows/NativePopupWindow.cs
+++ b/CursorLang.Agent/Windows/NativePopupWindow.cs
@@ -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.
///
- 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()
+ ///
+ /// 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
{
- 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());
- }
+ ///
+ /// 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
@@ -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);
}
diff --git a/CursorLang.Core/Interop/CaretNative.cs b/CursorLang.Core/Interop/CaretNative.cs
index d86019b..5a35409 100644
--- a/CursorLang.Core/Interop/CaretNative.cs
+++ b/CursorLang.Core/Interop/CaretNative.cs
@@ -113,7 +113,7 @@ internal static class CaretNative
private static PopupWindowNative.Rect? TryGetAutomationCaret()
{
// A hung application must not hang the popup along with it: we wait for the
- // answer for a limited time, otherwise we show the popup at the cursor
+ // answer for a limited time, otherwise the popup goes to the fixed point
Task query = Task.Run(QueryAutomationCaret);
return query.Wait(AutomationTimeout) ? query.Result : null;
}
@@ -165,7 +165,7 @@ internal static class CaretNative
}
#else
// Built without UI Automation: Chromium and Electron keep the system caret and MSAA
- // steps above, and where those stay silent the popup falls back to the cursor
+ // steps above, and where those stay silent the popup falls back to the fixed point
private static PopupWindowNative.Rect? TryGetAutomationCaret() => null;
#endif
diff --git a/CursorLang.Core/Models/PopupPlacement.cs b/CursorLang.Core/Models/PopupPlacement.cs
index d58a961..472cea4 100644
--- a/CursorLang.Core/Models/PopupPlacement.cs
+++ b/CursorLang.Core/Models/PopupPlacement.cs
@@ -10,7 +10,8 @@ public enum PopupPlacementMode
///
/// Next to the caret in the active input field. When the application does not
- /// report its position, the popup is shown at the mouse cursor.
+ /// report its position, the popup is shown as in — at the
+ /// place and with the look that mode is set up with.
///
AtCaret,
diff --git a/README.RU.md b/README.RU.md
index 7db9213..4284253 100644
--- a/README.RU.md
+++ b/README.RU.md
@@ -169,6 +169,15 @@ MSIX всегда выполняются в контексте вошедшег
на месте, а не исчезает из раздела — появляющаяся и исчезающая строка сдвигала бы
всё, что ниже, при каждой смене места.
+Положение каретки сообщает не всякое приложение, и тогда рядом с курсором ввода —
+нигде. Подсказка уходит на фиксированную точку: место, отступ и оформление берутся
+оттуда, из режима каретки не берётся ничего. Курсор мыши был бы запасным вариантом
+поближе — и неверным: он там, где его оставили, сбоку или на другом мониторе, тогда
+как смотрят на каретку. Фиксированная точка всегда на одном и том же месте, и потому
+известно, куда смотреть, — а вместе с местом должно приходить и оформление: подсказка,
+настроенная мелкой и незаметной для середины текста, в углу монитора останется
+незамеченной.
+
К какому режиму относится оформление, объясняет всплывающая подсказка рядом с самим
режимом, а не строка текста в разделе: ползунки ниже показывают другие числа после
переключения режима, и это вопрос, который задают один раз.
diff --git a/README.md b/README.md
index d71e85c..614d4ba 100644
--- a/README.md
+++ b/README.md
@@ -163,6 +163,15 @@ zero and the settings window shows it greyed out. It stays in place rather than
leaving the section — a row that comes and goes would move everything below it on
every switch of the place.
+Not every application reports where its caret is, and next to the caret is then
+nowhere. The popup goes to the fixed point instead — the place, the offset and the
+look all from that mode, none of them from the caret one. The mouse cursor would be
+the nearer fallback and is the wrong one: it is wherever it was last left, off to a
+side or on another monitor, while the eyes are on the caret. The fixed point is
+always in the same place, so it is known where to look — and the look must come with
+it, since a popup set small and quiet for the middle of a text goes unnoticed in the
+corner of a monitor.
+
Which mode the look belongs to is explained in a tooltip next to the mode itself
rather than by a line of text in the section: the sliders below show other numbers
after a switch of the mode, and that is a question asked once.