using System.Windows; using System.Windows.Interop; using CursorLang.Interop; using CursorLang.Models; using CursorLang.ViewModels; namespace CursorLang.Views; /// /// Всплывающая подсказка с коротким именем раскладки. /// Отвечает только за показ, размер и место на экране: когда её убрать, /// решает . /// public partial class LayoutPopupWindow : Window { private readonly AppSettings _settings; private Size _contentSize; public LayoutPopupWindow(LayoutPopupViewModel viewModel, AppSettings settings) { InitializeComponent(); DataContext = viewModel; _settings = settings; // Создаём окно заранее, чтобы задать его границы до первого показа: // иначе оно на мгновение появляется в размере по умолчанию new WindowInteropHelper(this).EnsureHandle(); } /// /// Показывает подсказку в месте, заданном настройками. /// public void ShowPopup() { MeasureContent(); // Уже созданное окно ставим на место до показа, чтобы оно не мелькнуло // на старой позиции; при самом первом вызове хэндла ещё нет ApplyBounds(); Show(); // Повторяем после показа: до него окно подчиняется системному минимальному // размеру окна и получается заметно крупнее текста ApplyBounds(); } protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); PopupWindowNative.MakePassive(new WindowInteropHelper(this).Handle); } // Размер содержимого нужен до показа: от него зависит и размер окна, // и позиция для углов. Рамок у окна нет, поэтому размер окна равен размеру // содержимого private void MeasureContent() { var content = (FrameworkElement)Content; content.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); _contentSize = content.DesiredSize; } // Всё считается в физических пикселях: у мониторов разный масштаб, а // Window.Left/Top/Width/Height пересчитываются по DPI того монитора, // где окно сейчас, и на соседнем мониторе дают промах 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()); } } // Точка привязки: каретка в поле ввода либо курсор мыши. Курсор — это // прямоугольник нулевого размера, поэтому расчёт углов у них общий 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); // У каждого режима привязки своя сторона и свой отступ 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 x, int y) = _settings.ScreenPosition switch { ScreenPosition.TopLeft => (work.Left + margin, work.Top + margin), ScreenPosition.TopRight => (work.Right - margin - width, work.Top + margin), ScreenPosition.BottomLeft => (work.Left + margin, work.Bottom - margin - height), ScreenPosition.BottomRight => (work.Right - margin - width, work.Bottom - margin - height), _ => (work.Left + ((work.Right - work.Left - width) / 2), work.Top + ((work.Bottom - work.Top - height) / 2)), }; PopupWindowNative.SetBounds(handle, x, y, width, height); } private static int ToPixels(double wpfUnits, double scale) => (int)Math.Round(wpfUnits * scale); }