Files
cursor-lang/CursorLang/Views/LayoutPopupWindow.xaml.cs
T
2026-08-09 20:02:23 +05:00

171 lines
6.1 KiB
C#

using System.Windows;
using System.Windows.Interop;
using CursorLang.Interop;
using CursorLang.Models;
using CursorLang.ViewModels;
namespace CursorLang.Views;
/// <summary>
/// The popup with the short name of the layout.
/// Responsible only for showing it, its size and its place on screen: when to take it
/// down is decided by <see cref="Services.LayoutPopupService"/>.
/// </summary>
public partial class LayoutPopupWindow : Window
{
private readonly AppSettings _settings;
private Size _contentSize;
public LayoutPopupWindow(LayoutPopupViewModel viewModel, AppSettings settings)
{
InitializeComponent();
DataContext = viewModel;
_settings = settings;
// We create the window in advance to set its bounds before the first show:
// otherwise it appears at the default size for a moment
new WindowInteropHelper(this).EnsureHandle();
}
/// <summary>
/// Shows the popup at the place set by the settings.
/// </summary>
public void ShowPopup()
{
MeasureContent();
// An already created window is put in place before the show so that it does
// not flash in its old position; on the very first call there is no handle yet
ApplyBounds();
Show();
// We repeat it after the show: before that the window is subject to the system
// minimum window size and comes out noticeably larger than its text
ApplyBounds();
}
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
PopupWindowNative.MakePassive(new WindowInteropHelper(this).Handle);
}
// The content size is needed before the show: both the window size and the corner
// position depend on it. The window has no frame, so the window size equals the
// content size
private void MeasureContent()
{
var content = (FrameworkElement)Content;
content.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
_contentSize = content.DesiredSize;
}
// Everything is computed in physical pixels: monitors have different scaling,
// while Window.Left/Top/Width/Height are converted using the DPI of the monitor
// the window is on right now, which misses the target on a neighbouring monitor
private void ApplyBounds()
{
IntPtr handle = new WindowInteropHelper(this).Handle;
if (handle == IntPtr.Zero)
{
return;
}
if (_settings.PlacementMode == PopupPlacementMode.FixedPoint)
{
ApplyBoundsOnScreen(handle);
}
else
{
ApplyBoundsNearAnchor(handle, GetAnchor());
}
}
// 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;
}
PopupWindowNative.Point cursor = PopupWindowNative.GetCursorPosition();
return new PopupWindowNative.Rect
{
Left = cursor.X,
Top = cursor.Y,
Right = cursor.X,
Bottom = cursor.Y,
};
}
private void ApplyBoundsNearAnchor(IntPtr handle, PopupWindowNative.Rect anchor)
{
var anchorPoint = new PopupWindowNative.Point { X = anchor.Left, Y = anchor.Top };
double scale = PopupWindowNative.GetScaleAt(anchorPoint);
// Every anchor mode has a side and an offset of its own
bool atCaret = _settings.PlacementMode == PopupPlacementMode.AtCaret;
AnchorSide side = atCaret ? _settings.CaretSide : _settings.CursorSide;
int offset = ToPixels(atCaret ? _settings.CaretOffset : _settings.CursorOffset, scale);
int width = ToPixels(_contentSize.Width, scale);
int height = ToPixels(_contentSize.Height, scale);
int toLeftOf = anchor.Left - offset - width;
int toRightOf = anchor.Right + offset;
int above = anchor.Top - offset - height;
int below = anchor.Bottom + offset;
// Для сторон «слева» и «справа» подсказка встаёт вровень с точкой привязки
int middle = anchor.Top + (((anchor.Bottom - anchor.Top) - height) / 2);
(int x, int y) = side switch
{
AnchorSide.TopLeft => (toLeftOf, above),
AnchorSide.TopRight => (toRightOf, above),
AnchorSide.Left => (toLeftOf, middle),
AnchorSide.Right => (toRightOf, middle),
AnchorSide.BottomLeft => (toLeftOf, below),
_ => (toRightOf, below),
};
PopupWindowNative.SetBounds(handle, x, y, width, height);
}
private void ApplyBoundsOnScreen(IntPtr handle)
{
(PopupWindowNative.Rect work, double scale) = PopupWindowNative.GetActiveMonitorWorkArea();
int width = ToPixels(_contentSize.Width, scale);
int height = ToPixels(_contentSize.Height, scale);
int margin = ToPixels(_settings.ScreenMargin, scale);
int left = work.Left + margin;
int right = work.Right - margin - width;
int top = work.Top + margin;
int bottom = work.Bottom - margin - height;
int centerX = work.Left + ((work.Right - work.Left - width) / 2);
int centerY = work.Top + ((work.Bottom - work.Top - height) / 2);
(int x, int y) = _settings.ScreenPosition switch
{
ScreenPosition.TopLeft => (left, top),
ScreenPosition.Top => (centerX, top),
ScreenPosition.TopRight => (right, top),
ScreenPosition.BottomLeft => (left, bottom),
ScreenPosition.Bottom => (centerX, bottom),
ScreenPosition.BottomRight => (right, bottom),
_ => (centerX, centerY),
};
PopupWindowNative.SetBounds(handle, x, y, width, height);
}
private static int ToPixels(double wpfUnits, double scale) => (int)Math.Round(wpfUnits * scale);
}