using System.Runtime.InteropServices; using System.Windows; using System.Windows.Interop; using CursorLang.Interop; using CursorLang.Models; using CursorLang.Services; using CursorLang.Tests.Infrastructure; using CursorLang.ViewModels; using CursorLang.Views; namespace CursorLang.Tests.Views; /// /// The tooltip window itself: where it ends up and how Windows sees it. /// public sealed class LayoutPopupWindowTests { private const int GwlExstyle = -20; private const int WsExNoactivate = 0x08000000; private const int WsExToolwindow = 0x00000080; [Fact] public void The_window_is_created_before_the_first_show() { Sta.Run(() => { using var popup = Popup.Create(new AppSettings()); // The handle is needed to set the window bounds before the show: // otherwise the window flashes at its default size for a moment Assert.NotEqual(IntPtr.Zero, popup.Handle); Assert.False(popup.Window.IsVisible); }); } [Fact] public void The_window_shows_what_the_view_model_gave_it() { Sta.Run(() => { var settings = new AppSettings(); using var popup = Popup.Create(settings); Assert.Same(popup.ViewModel, popup.Window.DataContext); popup.ViewModel.ShortName = "RU"; popup.Window.ShowPopup(); Assert.True(popup.Window.IsVisible); }); } [Fact] public void The_window_does_what_the_popup_service_expects_of_it() { Sta.Run(() => { using var popup = Popup.Create(new AppSettings()); Assert.IsAssignableFrom(popup.Window); }); } // The tooltip pops up over other applications and must neither take the // focus nor turn up in Alt+Tab [Fact] public void The_window_takes_no_focus_and_stays_out_of_the_switcher() { Sta.Run(() => { using var popup = Popup.Create(new AppSettings()); int style = GetWindowLong(popup.Handle, GwlExstyle); Assert.Equal(WsExNoactivate, style & WsExNoactivate); Assert.Equal(WsExToolwindow, style & WsExToolwindow); }); } [Fact] public void The_window_stays_on_top_and_out_of_the_mouses_way() { Sta.Run(() => { using var popup = Popup.Create(new AppSettings()); Assert.True(popup.Window.Topmost); Assert.False(popup.Window.ShowInTaskbar); Assert.False(popup.Window.ShowActivated); Assert.False(popup.Window.IsHitTestVisible); Assert.False(popup.Window.Focusable); Assert.Equal(WindowStyle.None, popup.Window.WindowStyle); Assert.True(popup.Window.AllowsTransparency); }); } [Fact] public void The_opacity_comes_from_the_settings() { Sta.Run(() => { var settings = new AppSettings { Opacity = 0.42 }; using var popup = Popup.Create(settings); popup.Window.ShowPopup(); Assert.Equal(0.42, popup.Window.Opacity, precision: 3); settings.Opacity = 0.75; Assert.Equal(0.75, popup.Window.Opacity, precision: 3); }); } [Theory] [InlineData(ScreenPosition.TopLeft)] [InlineData(ScreenPosition.Top)] [InlineData(ScreenPosition.TopRight)] [InlineData(ScreenPosition.Center)] [InlineData(ScreenPosition.BottomLeft)] [InlineData(ScreenPosition.Bottom)] [InlineData(ScreenPosition.BottomRight)] public void In_the_fixed_point_mode_the_window_lands_where_it_was_computed(ScreenPosition position) { Sta.Run(() => { var settings = new AppSettings { PlacementMode = PopupPlacementMode.FixedPoint, ScreenPosition = position, ScreenMargin = 24, }; using var popup = Popup.Create(settings); popup.ViewModel.ShortName = "RU"; (PopupWindowNative.Rect before, _) = PopupWindowNative.GetActiveMonitorWorkArea(); popup.Window.ShowPopup(); (PopupWindowNative.Rect work, double scale) = PopupWindowNative.GetActiveMonitorWorkArea(); if (!before.Equals(work)) { Assert.Skip("The active monitor changed while the check was running"); } PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(popup.Handle)!.Value; int width = bounds.Right - bounds.Left; int height = bounds.Bottom - bounds.Top; PopupWindowNative.Point expected = PopupLayout.OnScreen( work, position, PopupLayout.ToPixels(settings.ScreenMargin, scale), width, height); Assert.Equal(expected.X, bounds.Left); Assert.Equal(expected.Y, bounds.Top); }); } [Fact] public void At_the_cursor_the_window_lands_next_to_it() { Sta.Run(() => { var settings = new AppSettings { PlacementMode = PopupPlacementMode.AtCursor, CursorSide = AnchorSide.BottomRight, CursorOffset = 16, }; using var popup = Popup.Create(settings); popup.ViewModel.ShortName = "RU"; // The place is computed from where the cursor was at the moment of // the show. If it was moving right then, show it once more PopupWindowNative.Point before = default; for (int attempt = 0; attempt < 10; attempt++) { before = PopupWindowNative.GetCursorPosition(); popup.Window.ShowPopup(); PopupWindowNative.Point after = PopupWindowNative.GetCursorPosition(); if (before.X == after.X && before.Y == after.Y) { break; } if (attempt == 9) { Assert.Skip("The cursor kept moving the whole time"); } } PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(popup.Handle)!.Value; double scale = PopupWindowNative.GetScaleAt(before); PopupWindowNative.Point expected = PopupLayout.NearAnchor( PopupLayout.AsAnchor(before), AnchorSide.BottomRight, PopupLayout.ToPixels(settings.CursorOffset, scale), bounds.Right - bounds.Left, bounds.Bottom - bounds.Top); Assert.Equal(expected.X, bounds.Left); Assert.Equal(expected.Y, bounds.Top); }); } // There is no caret in the test environment, and the tooltip has to fall // back to the cursor [Fact] public void Without_a_caret_the_window_lands_at_the_cursor() { Sta.Run(() => { var settings = new AppSettings { PlacementMode = PopupPlacementMode.AtCaret, CaretSide = AnchorSide.BottomRight, CaretOffset = 8, }; using var popup = Popup.Create(settings); popup.ViewModel.ShortName = "RU"; PopupWindowNative.Point before = PopupWindowNative.GetCursorPosition(); popup.Window.ShowPopup(); PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(popup.Handle)!.Value; // A gentle check: a window holding the input focus may still have a caret Assert.True(bounds.Right > bounds.Left); Assert.True(bounds.Bottom > bounds.Top); Assert.NotEqual(default, before); }); } // The window size equals the size of the text: the tooltip has no frame [Fact] public void The_window_size_follows_the_size_of_the_caption() { Sta.Run(() => { var small = new AppSettings { FontSize = 12, PlacementMode = PopupPlacementMode.FixedPoint }; var large = new AppSettings { FontSize = 48, PlacementMode = PopupPlacementMode.FixedPoint }; using var smallPopup = Popup.Create(small); using var largePopup = Popup.Create(large); smallPopup.ViewModel.ShortName = "RU"; largePopup.ViewModel.ShortName = "RU"; smallPopup.Window.ShowPopup(); largePopup.Window.ShowPopup(); PopupWindowNative.Rect smallBounds = WindowPlacementNative.TryGetBounds(smallPopup.Handle)!.Value; PopupWindowNative.Rect largeBounds = WindowPlacementNative.TryGetBounds(largePopup.Handle)!.Value; Assert.True(largeBounds.Right - largeBounds.Left > smallBounds.Right - smallBounds.Left); Assert.True(largeBounds.Bottom - largeBounds.Top > smallBounds.Bottom - smallBounds.Top); }); } [Fact] public void Showing_again_moves_the_window_to_its_new_place() { Sta.Run(() => { var settings = new AppSettings { PlacementMode = PopupPlacementMode.FixedPoint, ScreenPosition = ScreenPosition.TopLeft, }; using var popup = Popup.Create(settings); popup.ViewModel.ShortName = "RU"; popup.Window.ShowPopup(); PopupWindowNative.Rect topLeft = WindowPlacementNative.TryGetBounds(popup.Handle)!.Value; settings.ScreenPosition = ScreenPosition.BottomRight; popup.Window.ShowPopup(); PopupWindowNative.Rect bottomRight = WindowPlacementNative.TryGetBounds(popup.Handle)!.Value; Assert.True(bottomRight.Left > topLeft.Left); Assert.True(bottomRight.Top > topLeft.Top); }); } [Fact] public void A_hidden_window_stays_alive() { Sta.Run(() => { using var popup = Popup.Create(new AppSettings()); popup.Window.ShowPopup(); popup.Window.Hide(); Assert.False(popup.Window.IsVisible); Assert.NotEqual(IntPtr.Zero, popup.Handle); popup.Window.ShowPopup(); Assert.True(popup.Window.IsVisible); }); } [DllImport("user32.dll", SetLastError = true)] private static extern int GetWindowLong(IntPtr hWnd, int nIndex); /// The tooltip window together with everything it needs to work. private sealed class Popup : IDisposable { private Popup(LayoutPopupWindow window, LayoutPopupViewModel viewModel) { Window = window; ViewModel = viewModel; Handle = new WindowInteropHelper(window).Handle; } internal LayoutPopupWindow Window { get; } internal LayoutPopupViewModel ViewModel { get; } internal IntPtr Handle { get; } internal static Popup Create(AppSettings settings) { var viewModel = new LayoutPopupViewModel(settings); return new Popup(new LayoutPopupWindow(viewModel, settings), viewModel); } public void Dispose() => Window.Close(); } }