using System.Windows; using System.Windows.Interop; using CursorLang.Interop; using CursorLang.Models; using CursorLang.ViewModels; namespace CursorLang.Views; /// /// 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 . /// 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(); } /// /// Shows the popup at the place set by the settings. /// 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); }