using System.Numerics; using AcDream.App.UI; using AcDream.App.UI.Layout; using Xunit; namespace AcDream.App.Tests.UI; /// /// Campaign LA gate round 2 (register AD-98): retail renders fixed-canvas /// pre-world screens (char select, authored 800×600) at authored size and /// stretches the whole composed frame at presentation — its UI blitter has no /// stretch mode. acdream's equivalent is : /// draw applies one uniform scale at the renderer's quad chokepoint, and the /// mouse entry points apply the exact inverse so hit-testing lives in canvas /// space. These tests pin the scale math and the inverse input mapping — the /// half that, if wrong, makes the user click on art and hit nothing. /// public class UiRootFixedCanvasTests { [Fact] public void CanvasScale_IsOne_WithoutFixedCanvas() { var root = new UiRoot { Width = 1920f, Height = 1080f }; Assert.Equal(Vector2.One, root.CanvasScale); } [Fact] public void CanvasScale_IsWindowOverCanvas_WhenFixed() { var root = new UiRoot { Width = 1920f, Height = 1080f, FixedCanvasSize = new Vector2(800f, 600f), }; Assert.Equal(new Vector2(2.4f, 1.8f), root.CanvasScale); } [Fact] public void CanvasScale_IsOne_ForDegenerateCanvasOrWindow() { var zeroCanvas = new UiRoot { Width = 1920f, Height = 1080f, FixedCanvasSize = new Vector2(0f, 600f), }; Assert.Equal(Vector2.One, zeroCanvas.CanvasScale); var zeroWindow = new UiRoot { Width = 0f, Height = 0f, FixedCanvasSize = new Vector2(800f, 600f), }; Assert.Equal(Vector2.One, zeroWindow.CanvasScale); } /// /// The load-bearing inverse: a click at WINDOW coordinates must land on the /// widget whose authored CANVAS rect the user visually clicked. The button /// sits at canvas (300,400)+(120×40); at a 1920×1080 window over an 800×600 /// canvas it appears at window (720,720)-(1008,792). Clicking window /// (860,750) — canvas (358,417) — must click it; clicking window (300,400) /// — canvas (125,222), visually empty — must not. /// [Fact] public void MouseInput_MapsWindowCoordsToCanvasSpace() { var root = new UiRoot { Width = 1920f, Height = 1080f, FixedCanvasSize = new Vector2(800f, 600f), }; int clicks = 0; var button = new UiButton( new ElementInfo { Width = 120, Height = 40 }, _ => (0u, 0, 0)) { Left = 300f, Top = 400f, Width = 120f, Height = 40f, OnClick = () => clicks++, }; root.AddChild(button); // Truncation, not rounding (batch review F6): 860/2.4 = 358.33 → 358, // 750/1.8 = 416.67 → 416. Rounding mapped the window's far edge one // past the canvas's last valid coordinate (1919 → 800), losing a 1px // hit-test band at the right/bottom. root.OnMouseDown(UiMouseButton.Left, 860, 750); root.OnMouseUp(UiMouseButton.Left, 860, 750); Assert.Equal(1, clicks); Assert.Equal(358, root.MouseX); Assert.Equal(416, root.MouseY); // The far window edge maps INSIDE the canvas. root.OnMouseMove(1919, 1079); Assert.Equal(799, root.MouseX); Assert.Equal(599, root.MouseY); root.OnMouseDown(UiMouseButton.Left, 300, 400); root.OnMouseUp(UiMouseButton.Left, 300, 400); Assert.Equal(1, clicks); } [Fact] public void MouseInput_IsUntouched_WithoutFixedCanvas() { var root = new UiRoot { Width = 1920f, Height = 1080f }; int clicks = 0; var button = new UiButton( new ElementInfo { Width = 120, Height = 40 }, _ => (0u, 0, 0)) { Left = 300f, Top = 400f, Width = 120f, Height = 40f, OnClick = () => clicks++, }; root.AddChild(button); root.OnMouseDown(UiMouseButton.Left, 360, 420); root.OnMouseUp(UiMouseButton.Left, 360, 420); Assert.Equal(1, clicks); } }