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>
121 lines
3.8 KiB
C#
121 lines
3.8 KiB
C#
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);
|
||
}
|
||
}
|