acdream/tests/AcDream.App.Tests/UI/UiRootFixedCanvasTests.cs
Erik 73041d7015 fix(ui): Campaign LA gate round 2 — character-select scales as one authored canvas
Third iteration on the screen, completing AD-98. The previous substitution
stretched only the root BACKGROUND while the child widgets stayed at their
authored 800x600 pixel positions - and the background painting carries
visual anchors (the World/Characters captions are art), so the user gate
showed captions overlapping the listbox and every widget misaligned
against the stretched art.

Retail model (established at 71bf24fb): fixed-canvas pre-world screens
render at authored 800x600 and the whole composed frame stretches once at
presentation; the blitter has no stretch mode. Our equivalent now does the
same one stage earlier:

- UiRoot.FixedCanvasSize: while the char-select screen is active, the
  retained tree lays out in its authored canvas and Draw scopes a uniform
  scale onto TextRenderer.CanvasScale; the mouse entry points apply the
  exact inverse so MouseX/MouseY and every hit test live in canvas space.
- TextRenderer.AppendQuad is the single emission chokepoint - sprites,
  rects, AND glyphs scale together, including retail-authentic non-uniform
  aspect distortion and stretched text. World-space HUD stays native (the
  scale resets outside UiRoot.Draw).
- CharacterManagementUiController stops resizing Root to the viewport;
  activate/deactivate/dispose set and clear the host canvas.
- UiDatElement returns to retail-pure copy-or-tile; the interim
  StretchOwnBackgroundToFill flag is deleted.
- AD-98 updated to describe the completed substitution.

Tests: canvas-scale quad math, inverse input mapping (window click lands
on the canvas-space widget), degenerate-size guards, controller keeps
authored extent + sets/clears the canvas. App suite 5085/6 skips; live-DAT
char-select probes 3/3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 10:39:19 +02:00

121 lines
3.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Numerics;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using Xunit;
namespace AcDream.App.Tests.UI;
/// <summary>
/// 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 <see cref="UiRoot.FixedCanvasSize"/>:
/// 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.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
[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);
root.OnMouseDown(UiMouseButton.Left, 860, 750);
root.OnMouseUp(UiMouseButton.Left, 860, 750);
Assert.Equal(1, clicks);
Assert.Equal(358, root.MouseX);
Assert.Equal(417, 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);
}
}