289 lines
9.6 KiB
C#
289 lines
9.6 KiB
C#
using System.Windows;
|
|
using System.Windows.Interop;
|
|
using CursorLang.Core.Interop;
|
|
using CursorLang.Settings.Interop;
|
|
using CursorLang.Settings.Services;
|
|
using CursorLang.Settings.Tests.Infrastructure;
|
|
|
|
namespace CursorLang.Settings.Tests.Services;
|
|
|
|
/// <summary>
|
|
/// Placing the settings window: the centre maths and bringing the window back
|
|
/// into the work area.
|
|
/// </summary>
|
|
public sealed class MainWindowPlacementTests
|
|
{
|
|
private static readonly PopupWindowNative.Rect Work = new()
|
|
{
|
|
Left = 0,
|
|
Top = 0,
|
|
Right = 1000,
|
|
Bottom = 800,
|
|
};
|
|
|
|
private static readonly PopupWindowNative.Rect Bounds = new()
|
|
{
|
|
Left = 0,
|
|
Top = 0,
|
|
Right = 400,
|
|
Bottom = 300,
|
|
};
|
|
|
|
[Fact]
|
|
public void The_centre_follows_the_window_size_and_the_work_area()
|
|
{
|
|
PopupWindowNative.Point point = MainWindowPlacement.Center(Bounds, Work);
|
|
|
|
Assert.Equal((1000 - 400) / 2, point.X);
|
|
Assert.Equal((800 - 300) / 2, point.Y);
|
|
}
|
|
|
|
// The work area of a second monitor does not start at zero
|
|
[Fact]
|
|
public void The_centre_of_a_neighbouring_monitor_is_measured_from_its_left_edge()
|
|
{
|
|
var work = new PopupWindowNative.Rect { Left = 1920, Top = 100, Right = 3520, Bottom = 1000 };
|
|
|
|
PopupWindowNative.Point point = MainWindowPlacement.Center(Bounds, work);
|
|
|
|
Assert.Equal(1920 + ((1600 - 400) / 2), point.X);
|
|
Assert.Equal(100 + ((900 - 300) / 2), point.Y);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_window_inside_the_work_area_stays_where_it_is()
|
|
{
|
|
var position = new PopupWindowNative.Point { X = 120, Y = 90 };
|
|
|
|
PopupWindowNative.Point clamped = MainWindowPlacement.Clamp(position, Bounds, Work);
|
|
|
|
Assert.Equal(120, clamped.X);
|
|
Assert.Equal(90, clamped.Y);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_window_past_the_right_edge_is_pulled_back_in()
|
|
{
|
|
var position = new PopupWindowNative.Point { X = 900, Y = 700 };
|
|
|
|
PopupWindowNative.Point clamped = MainWindowPlacement.Clamp(position, Bounds, Work);
|
|
|
|
Assert.Equal(1000 - 400, clamped.X);
|
|
Assert.Equal(800 - 300, clamped.Y);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_window_past_the_left_and_top_edges_is_pulled_back_in()
|
|
{
|
|
var position = new PopupWindowNative.Point { X = -500, Y = -400 };
|
|
|
|
PopupWindowNative.Point clamped = MainWindowPlacement.Clamp(position, Bounds, Work);
|
|
|
|
Assert.Equal(Work.Left, clamped.X);
|
|
Assert.Equal(Work.Top, clamped.Y);
|
|
}
|
|
|
|
// The window height matches its content and on a short monitor exceeds the
|
|
// work area. The title bar matters more than the bottom of the window
|
|
[Fact]
|
|
public void A_window_taller_than_the_work_area_is_pinned_to_its_top()
|
|
{
|
|
var tall = new PopupWindowNative.Rect { Left = 0, Top = 0, Right = 400, Bottom = 900 };
|
|
var position = new PopupWindowNative.Point { X = 0, Y = 300 };
|
|
|
|
PopupWindowNative.Point clamped = MainWindowPlacement.Clamp(position, tall, Work);
|
|
|
|
Assert.Equal(Work.Top, clamped.Y);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_window_wider_than_the_work_area_is_pinned_to_its_left_edge()
|
|
{
|
|
var wide = new PopupWindowNative.Rect { Left = 0, Top = 0, Right = 1200, Bottom = 300 };
|
|
var position = new PopupWindowNative.Point { X = 400, Y = 0 };
|
|
|
|
PopupWindowNative.Point clamped = MainWindowPlacement.Clamp(position, wide, Work);
|
|
|
|
Assert.Equal(Work.Left, clamped.X);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0, 0, 0, 0, true)]
|
|
[InlineData(0, 0, 100, 0, true)]
|
|
[InlineData(0, 0, 0, 100, true)]
|
|
[InlineData(100, 100, 100, 200, true)]
|
|
[InlineData(0, 0, 1, 1, false)]
|
|
[InlineData(-100, -100, 100, 100, false)]
|
|
public void An_area_without_width_or_height_counts_as_empty(
|
|
int left, int top, int right, int bottom, bool expected)
|
|
{
|
|
var rect = new PopupWindowNative.Rect { Left = left, Top = top, Right = right, Bottom = bottom };
|
|
|
|
Assert.Equal(expected, MainWindowPlacement.IsEmpty(rect));
|
|
}
|
|
|
|
[Fact]
|
|
public void The_first_time_in_a_session_the_window_lands_centred_on_the_active_monitor()
|
|
{
|
|
Sta.Run(() =>
|
|
{
|
|
var placement = new MainWindowPlacement();
|
|
using var window = new TestWindow();
|
|
|
|
(PopupWindowNative.Rect before, _) = PopupWindowNative.GetActiveMonitorWorkArea();
|
|
placement.Apply(window);
|
|
(PopupWindowNative.Rect work, _) = PopupWindowNative.GetActiveMonitorWorkArea();
|
|
|
|
if (!before.Equals(work))
|
|
{
|
|
// The user moved to another monitor right during the check
|
|
Assert.Skip("The active monitor changed while the check was running");
|
|
}
|
|
|
|
PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(window.Handle)!.Value;
|
|
|
|
// Pixel precision: the window goes exactly where it was computed to go
|
|
PopupWindowNative.Point expected = MainWindowPlacement.Clamp(
|
|
MainWindowPlacement.Center(bounds, work), bounds, work);
|
|
|
|
Assert.Equal(expected.X, bounds.Left);
|
|
Assert.Equal(expected.Y, bounds.Top);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void The_window_returns_where_the_user_moved_it()
|
|
{
|
|
Sta.Run(() =>
|
|
{
|
|
var placement = new MainWindowPlacement();
|
|
using var window = new TestWindow();
|
|
|
|
placement.Attach(window);
|
|
placement.Apply(window);
|
|
|
|
// Move the window the way the user does it with the mouse
|
|
PopupWindowNative.Rect centered = WindowPlacementNative.TryGetBounds(window.Handle)!.Value;
|
|
PopupWindowNative.MoveTo(window.Handle, centered.Left + 40, centered.Top + 30);
|
|
window.RaiseLocationChanged();
|
|
|
|
// Showing the window again — it has to stay where it was left
|
|
placement.Apply(window);
|
|
|
|
PopupWindowNative.Rect after = WindowPlacementNative.TryGetBounds(window.Handle)!.Value;
|
|
Assert.Equal(centered.Left + 40, after.Left);
|
|
Assert.Equal(centered.Top + 30, after.Top);
|
|
});
|
|
}
|
|
|
|
// While we move the window ourselves its position must not drift from repeats
|
|
[Fact]
|
|
public void Placing_again_does_not_move_the_window()
|
|
{
|
|
Sta.Run(() =>
|
|
{
|
|
var placement = new MainWindowPlacement();
|
|
using var window = new TestWindow();
|
|
|
|
placement.Attach(window);
|
|
placement.Apply(window);
|
|
|
|
PopupWindowNative.Rect first = WindowPlacementNative.TryGetBounds(window.Handle)!.Value;
|
|
|
|
placement.Apply(window);
|
|
placement.Apply(window);
|
|
|
|
PopupWindowNative.Rect third = WindowPlacementNative.TryGetBounds(window.Handle)!.Value;
|
|
Assert.Equal(first.Left, third.Left);
|
|
Assert.Equal(first.Top, third.Top);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void A_minimised_window_is_not_placed()
|
|
{
|
|
Sta.Run(() =>
|
|
{
|
|
var placement = new MainWindowPlacement();
|
|
using var window = new TestWindow();
|
|
|
|
PopupWindowNative.MoveTo(window.Handle, 7, 9);
|
|
window.WindowState = WindowState.Minimized;
|
|
|
|
placement.Apply(window);
|
|
|
|
PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(window.Handle)!.Value;
|
|
Assert.Equal(7, bounds.Left);
|
|
Assert.Equal(9, bounds.Top);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void A_window_without_a_handle_is_not_placed()
|
|
{
|
|
Sta.Run(() =>
|
|
{
|
|
var placement = new MainWindowPlacement();
|
|
var window = new Window { Width = 200, Height = 150 };
|
|
|
|
// There must be no exception: the window does not exist yet,
|
|
// so there is nothing to place
|
|
placement.Apply(window);
|
|
|
|
Assert.Equal(IntPtr.Zero, new WindowInteropHelper(window).Handle);
|
|
});
|
|
}
|
|
|
|
// The place of the window lives in memory only: the set of monitors may be
|
|
// different by the next run
|
|
[Fact]
|
|
public void Every_placement_starts_its_session_afresh()
|
|
{
|
|
Sta.Run(() =>
|
|
{
|
|
using var window = new TestWindow();
|
|
|
|
var first = new MainWindowPlacement();
|
|
first.Attach(window);
|
|
first.Apply(window);
|
|
|
|
PopupWindowNative.Rect centered = WindowPlacementNative.TryGetBounds(window.Handle)!.Value;
|
|
|
|
PopupWindowNative.MoveTo(window.Handle, centered.Left + 60, centered.Top + 60);
|
|
window.RaiseLocationChanged();
|
|
|
|
// A new placement knows nothing of the earlier move and centres the window again
|
|
var second = new MainWindowPlacement();
|
|
second.Apply(window);
|
|
|
|
PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(window.Handle)!.Value;
|
|
Assert.Equal(centered.Left, bounds.Left);
|
|
Assert.Equal(centered.Top, bounds.Top);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// A window with a ready handle that never appears on screen: placement
|
|
/// works with the system bounds, and a created window is enough for those.
|
|
/// </summary>
|
|
private sealed class TestWindow : Window, IDisposable
|
|
{
|
|
internal TestWindow()
|
|
{
|
|
Width = 400;
|
|
Height = 300;
|
|
ShowInTaskbar = false;
|
|
WindowStartupLocation = WindowStartupLocation.Manual;
|
|
|
|
Handle = new WindowInteropHelper(this).EnsureHandle();
|
|
}
|
|
|
|
internal IntPtr Handle { get; }
|
|
|
|
/// <summary>Reports a move the way WPF does after the user acts.</summary>
|
|
internal void RaiseLocationChanged() => OnLocationChanged(EventArgs.Empty);
|
|
|
|
public void Dispose() => Close();
|
|
}
|
|
}
|