using System.Windows; using System.Windows.Interop; using System.Windows.Media; using System.Windows.Threading; using CursorLang.Core.Interop; using CursorLang.Settings.Interop; namespace CursorLang.Settings.Services; /// /// Decides where the settings window shows up: for the first time in a session — in /// the centre of the monitor the user is working on, and after that — where they /// left that window. /// /// /// The position lives in memory only and is not kept between launches: the set of /// monitors may be different by the next launch, while "in the centre of the active /// one" is always right. /// public sealed class MainWindowPlacement { private PopupWindowNative.Point? _position; // The window reports a move when we move it ourselves as well; // what has to be remembered is only what the user chose private bool _isPlacing; // Captured once, before anything narrows MaxHeight to a particular monitor — // otherwise a later, more generous monitor would stay capped at whatever a // previous, shorter one left behind private double? _designMaxHeight; // Captured once, so a DPI change has a known-correct value to reassert — see // RestoreDesignWidth private double? _designWidth; /// /// Takes over the placement of the window: puts it in place by the first show /// and follows where the user moves it. /// public void Attach(Window window) { window.SourceInitialized += OnSourceInitialized; window.LocationChanged += OnLocationChanged; window.DpiChanged += OnDpiChanged; } /// /// Returns the window to the remembered place, and when it has not been shown /// yet during this session — puts it in the centre of the active monitor. /// public void Apply(Window window) { // A minimized window has no meaningful bounds: it is restored in its former // place, and it can be positioned only after that if (window.WindowState != WindowState.Normal) { return; } IntPtr handle = new WindowInteropHelper(window).Handle; if (handle == IntPtr.Zero || WindowPlacementNative.TryGetBounds(handle) is not { } bounds) { return; } PopupWindowNative.Point? wanted = _position ?? CenterOnActiveMonitor(bounds); if (wanted is null || KeepOnScreen(wanted.Value, bounds) is not { } target) { return; } _isPlacing = true; try { PopupWindowNative.MoveTo(handle, target.X, target.Y); } finally { _isPlacing = false; } _position = target; } // The window height adapts to its content and is unknown until the first layout // pass — an empty window frame would end up in the centre. So we ask for the // layout to be computed right away: by that moment the window is not shown yet, // so it will not flash in its former place private void OnSourceInitialized(object? sender, EventArgs e) { if (sender is not Window window) { return; } window.SourceInitialized -= OnSourceInitialized; _designMaxHeight = window.MaxHeight; _designWidth = window.Width; LimitHeightToOwnMonitor(window); window.UpdateLayout(); Apply(window); } // A monitor is not necessarily final at creation time — Windows may place the new // window on one monitor before Apply moves it to another, and the user is free to // drag it to a third one later. Every one of those is a real DPI change, and this // runs again for each: recomputing the cap from whichever monitor holds the window // right now, rather than trusting a value worked out for a previous one. private void OnDpiChanged(object sender, DpiChangedEventArgs e) { if (sender is not Window window) { return; } // Deferred rather than run inline: this event fires while WPF is still in // the middle of its own response to the same DPI change (its per-monitor // rescale of Width touches it after this handler if the fix-up runs // synchronously, undoing it). Posting behind that on the dispatcher queue // lets our fix-up run once WPF's own pass has finished. window.Dispatcher.BeginInvoke(DispatcherPriority.ContextIdle, new Action(() => { LimitHeightToOwnMonitor(window); RestoreDesignWidth(window); ReapplySizeToContent(window); })); } // The width is a plain, explicit value rather than something SizeToContent // computes, and WPF's own per-monitor rescaling does not reliably keep it at the // same logical width when the system's scaling changes live under an // already-open window, as opposed to the window being dragged onto a different // monitor — it can come out scaled by roughly the ratio between the old and the // new DPI instead of staying put. Reasserting the original value here is simpler // than chasing exactly where that rescale goes wrong. private void RestoreDesignWidth(Window window) { if (_designWidth is { } designWidth) { window.Width = designWidth; } } // UpdateLayout alone settles the measure/arrange pass of the visual tree, but // does not reliably make WPF redo its own step of resizing the native window to // match SizeToContent when the change originates from a DPI event rather than an // ordinary content change. Left alone, the window can end up either too tall — a // blank strip below the real content, once MaxHeight has just pulled the content // shorter — or stuck too short after being dragged back to a monitor with room to // grow again. Turning SizeToContent off and back on forces that resizing step to // run again from scratch. private static void ReapplySizeToContent(Window window) { SizeToContent original = window.SizeToContent; window.SizeToContent = SizeToContent.Manual; window.SizeToContent = original; window.UpdateLayout(); } // MaxHeight in XAML is a constant tuned for an ordinary desktop monitor at 100% // scaling. At a high scale factor the same number of device-independent pixels // turns into more physical pixels than a short or heavily scaled monitor has, and // SizeToContent then grows the window past the bottom of the screen instead of // asking the ScrollViewer inside it to scroll — with nothing to grab, the excess // is unreachable. Capping MaxHeight to what the window's own monitor really offers // keeps the whole window on screen and lets the ScrollViewer take over. private void LimitHeightToOwnMonitor(Window window) { if (_designMaxHeight is not { } designMaxHeight) { return; } IntPtr handle = new WindowInteropHelper(window).Handle; if (handle == IntPtr.Zero || WindowPlacementNative.TryGetBounds(handle) is not { } bounds || WindowPlacementNative.TryGetWorkAreaNear(bounds) is not { } work || IsEmpty(work)) { return; } double scale = VisualTreeHelper.GetDpi(window).DpiScaleY; if (scale <= 0) { return; } // Leaves room for the title bar and a margin from the edges of the screen, // in the same units as the work area height once the monitor's scale is // divided out const double reservedForChrome = 80; double workAreaHeight = (work.Bottom - work.Top) / scale; window.MaxHeight = Math.Min(designMaxHeight, Math.Max(200, workAreaHeight - reservedForChrome)); } private void OnLocationChanged(object? sender, EventArgs e) { if (_isPlacing || sender is not Window { WindowState: WindowState.Normal } window) { return; } IntPtr handle = new WindowInteropHelper(window).Handle; if (handle != IntPtr.Zero && WindowPlacementNative.TryGetBounds(handle) is { } bounds) { _position = new PopupWindowNative.Point { X = bounds.Left, Y = bounds.Top }; } } private static PopupWindowNative.Point? CenterOnActiveMonitor(PopupWindowNative.Rect bounds) { (PopupWindowNative.Rect work, _) = PopupWindowNative.GetActiveMonitorWorkArea(); return IsEmpty(work) ? null : Center(bounds, work); } /// /// The point at which a window with the given bounds ends up in the centre of the work area. /// internal static PopupWindowNative.Point Center( PopupWindowNative.Rect bounds, PopupWindowNative.Rect work) => new() { X = work.Left + (((work.Right - work.Left) - Width(bounds)) / 2), Y = work.Top + (((work.Bottom - work.Top) - Height(bounds)) / 2), }; /// /// Pulls the window into the work area of the nearest monitor. /// /// /// Needed in two cases. The monitor the user put the window on may be disconnected /// during the session — returning the window to its place would then leave the /// user without a window, so the remembered position is a wish here rather than an /// order. And the window height equals the height of its content and may exceed /// the work area on a short monitor — then the title bar of a window placed in the /// centre would go past the top edge. /// private static PopupWindowNative.Point? KeepOnScreen( PopupWindowNative.Point position, PopupWindowNative.Rect bounds) { int width = Width(bounds); int height = Height(bounds); var wanted = new PopupWindowNative.Rect { Left = position.X, Top = position.Y, Right = position.X + width, Bottom = position.Y + height, }; if (WindowPlacementNative.TryGetWorkAreaNear(wanted) is not { } work || IsEmpty(work)) { return null; } return Clamp(position, bounds, work); } /// /// Pulls the point so that a window with the given bounds fits into the work area /// entirely. A window taller than the work area gets its top edge: the title bar /// is needed more than the lower part of the window. /// internal static PopupWindowNative.Point Clamp( PopupWindowNative.Point position, PopupWindowNative.Rect bounds, PopupWindowNative.Rect work) { int width = Width(bounds); int height = Height(bounds); return new PopupWindowNative.Point { X = Math.Clamp(position.X, work.Left, Math.Max(work.Left, work.Right - width)), Y = Math.Clamp(position.Y, work.Top, Math.Max(work.Top, work.Bottom - height)), }; } private static int Width(PopupWindowNative.Rect rect) => rect.Right - rect.Left; private static int Height(PopupWindowNative.Rect rect) => rect.Bottom - rect.Top; internal static bool IsEmpty(PopupWindowNative.Rect rect) => rect.Right <= rect.Left || rect.Bottom <= rect.Top; }