using System.Numerics; using AcDream.App.Rendering; using AcDream.App.Rendering.Gpu; using AcDream.App.Tests.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; namespace AcDream.App.Tests.UI.Layout; public class UiDatElementTests { private sealed class NullGpuFrameSource : ICurrentGpuFrameSource { public IGpuFrame? CurrentFrame => null; } private static (TextRenderer renderer, UiRenderContext ctx) BuildRenderContext() { var device = new RecordingGpuDevice(); var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused"); renderer.Begin(new Vector2(1920f, 1080f)); var ctx = new UiRenderContext(renderer, new Vector2(1920f, 1080f)); return (renderer, ctx); } /// /// Retail's blit is copy-or-tile only (Graphic::Draw @0x00693b20 — no /// stretch mode exists), so an element grown past its media's native size /// tiles: u1 = Width/tw. Whole-screen stretching is NOT this layer's job — /// it happens uniformly at UiRoot.FixedCanvasSize / TextRenderer.CanvasScale /// (register AD-98), covered by the test below. /// [Fact] public void OwnBackground_TilesUvPastOne_WhenRectExceedsNativeSize() { var info = new ElementInfo { Width = 1920, Height = 1080 }; info.StateMedia[""] = (0x06007576u, 1); var e = new UiDatElement(info, _ => (7u, 800, 600)) { Left = 0, Top = 0, Width = 1920, Height = 1080, }; (TextRenderer renderer, UiRenderContext ctx) = BuildRenderContext(); e.DrawSelfAndChildren(ctx); var (texture, verts) = Assert.Single(renderer.DebugSpriteSegmentVerts); Assert.Equal(7u, texture); // Vertex layout: x,y,u,v,r,g,b,a, 6 verts/quad. TextRenderer.AppendQuad emits // vertex index 1 as (x+w, y+h, u1, v1) — the (u1,v1) far corner. float uMax = verts[1 * 8 + 2]; float vMax = verts[1 * 8 + 3]; Assert.Equal(1920f / 800f, uMax, 3); Assert.Equal(1080f / 600f, vMax, 3); } /// /// AD-98 whole-canvas stretch: with the renderer's CanvasScale set (the /// char-select 800×600 canvas on a 1920×1080 window), an element drawn at /// authored size emits a quad scaled by exactly (2.4, 1.8) in GEOMETRY while /// its UVs stay authored (0..1 here) — one stretched image, no tiling, the /// same math retail's present-time frame stretch produces. /// [Fact] public void CanvasScale_StretchesQuadGeometry_LeavesUvsAuthored() { var info = new ElementInfo { Width = 800, Height = 600 }; info.StateMedia[""] = (0x06007576u, 1); var e = new UiDatElement(info, _ => (7u, 800, 600)) { Left = 0, Top = 0, Width = 800, Height = 600, }; (TextRenderer renderer, UiRenderContext ctx) = BuildRenderContext(); renderer.CanvasScale = new Vector2(1920f / 800f, 1080f / 600f); try { e.DrawSelfAndChildren(ctx); } finally { renderer.CanvasScale = Vector2.One; } var (_, verts) = Assert.Single(renderer.DebugSpriteSegmentVerts); // Far corner (vertex 1): geometry scaled to the window, UVs authored. Assert.Equal(1920f, verts[1 * 8 + 0], 3); Assert.Equal(1080f, verts[1 * 8 + 1], 3); Assert.Equal(1f, verts[1 * 8 + 2], 3); Assert.Equal(1f, verts[1 * 8 + 3], 3); } [Fact] public void ActiveMedia_PrefersNamedStateOverDirect() { var info = new ElementInfo(); info.StateMedia[""] = (0x06000001, 1); // DirectState (DrawMode Normal=1) info.StateMedia["ShowDetail"] = (0x06000002, 3); // named (Alphablend=3) var e = new UiDatElement(info, _ => (0, 0, 0)) { ActiveState = "ShowDetail" }; Assert.Equal(0x06000002u, e.ActiveMedia().File); Assert.Equal(3, e.ActiveMedia().DrawMode); e.ActiveState = ""; Assert.Equal(0x06000001u, e.ActiveMedia().File); Assert.Equal(1, e.ActiveMedia().DrawMode); } [Fact] public void ActiveMedia_NoMedia_ReturnsZero() { var e = new UiDatElement(new ElementInfo(), _ => (0, 0, 0)); Assert.Equal(0u, e.ActiveMedia().File); Assert.Equal(0, e.ActiveMedia().DrawMode); } [Fact] public void ActiveMedia_MissingNamedState_FallsBackToDirect() { var info = new ElementInfo(); info.StateMedia[""] = (0x06000005, 1); var e = new UiDatElement(info, _ => (0, 0, 0)) { ActiveState = "NoSuchState" }; Assert.Equal(0x06000005u, e.ActiveMedia().File); } // ── G1 tests: DefaultStateName + "Normal" implicit default ─────────────── /// /// Task G1 change 5: when an element has no DefaultStateName but does have a "Normal" /// state sprite, the ctor should default ActiveState to "Normal" so the element /// renders its normal-state sprite without requiring explicit state assignment. /// [Fact] public void UiDatElement_DefaultsActiveStateToNormal_WhenNormalPresent() { var info = new ElementInfo(); info.StateMedia["Normal"] = (0x0000AAAAu, 1); info.StateMedia["Hover"] = (0x0000BBBBu, 1); var e = new UiDatElement(info, _ => (0, 0, 0)); // Should have defaulted to "Normal" state. Assert.Equal(0x0000AAAAu, e.ActiveMedia().File); } /// /// Task G1 change 5: when DefaultStateName is set (e.g. "Minimized"), /// it takes priority over the "Normal" implicit default. /// [Fact] public void UiDatElement_DefaultsActiveStateToDefaultStateName_WhenSet() { var info = new ElementInfo { DefaultStateName = "Minimized" }; info.StateMedia["Minimized"] = (0x0000BBBBu, 1); info.StateMedia["Maximized"] = (0x0000CCCCu, 1); info.StateMedia["Normal"] = (0x0000DDDDu, 1); var e = new UiDatElement(info, _ => (0, 0, 0)); // DefaultStateName "Minimized" wins over "Normal" implicit default. Assert.Equal(0x0000BBBBu, e.ActiveMedia().File); } /// /// Task G1 change 5: elements with only a DirectState sprite and no "Normal" state /// should still default to "" (DirectState) — no regression for chrome/grip elements. /// [Fact] public void UiDatElement_NoDefaultStateName_NoNormal_DefaultsToDirectState() { var info = new ElementInfo(); info.StateMedia[""] = (0x06007777u, 1); // DirectState only (e.g. vitals chrome corner) var e = new UiDatElement(info, _ => (0, 0, 0)); // No DefaultStateName, no "Normal" state → ActiveState stays "" (DirectState). Assert.Equal(0x06007777u, e.ActiveMedia().File); } [Fact] public void NumericStateBridge_SetsNamedStateWithoutChangingGeometry() { var info = new ElementInfo { X = 4, Y = 5, Width = 30, Height = 40 }; info.States[RetailUiStateIds.ShowDetail] = new UiStateInfo { Id = RetailUiStateIds.ShowDetail, Name = "ShowDetail", Image = new UiImageMedia(0x06000009u, 3), }; info.StateMedia["ShowDetail"] = (0x06000009u, 3); var element = new UiDatElement(info, _ => (0u, 0, 0)) { Left = info.X, Top = info.Y, Width = info.Width, Height = info.Height, }; Assert.True(element.TrySetRetailState(RetailUiStateIds.ShowDetail)); Assert.Equal(RetailUiStateIds.ShowDetail, element.ActiveRetailStateId); Assert.Equal((0x06000009u, 3), element.ActiveMedia()); Assert.Equal((4f, 5f, 30f, 40f), (element.Left, element.Top, element.Width, element.Height)); } [Fact] public void NumericStateBridge_MissingStatePreservesCurrentState() { var info = new ElementInfo { DefaultStateName = "Normal" }; info.StateMedia["Normal"] = (0x06000001u, 1); var element = new UiDatElement(info, _ => (0u, 0, 0)); Assert.False(element.TrySetRetailState(RetailUiStateIds.ShowDetail)); Assert.Equal("Normal", element.ActiveState); } }