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; using AcDream.Core.Chat; using AcDream.UI.Abstractions; using AcDream.UI.Abstractions.Panels.Chat; namespace AcDream.App.Tests.UI.Layout; /// /// Dat-free conformance tests for the committed chat_2100006f.json golden fixture — /// retail's ACTUAL main chat window (LayoutDesc 0x2100006F, window root /// 0x10000600, authored 410x100; Campaign CH slice CH6a). Verifies that /// LayoutImporter.ImportInfos correctly resolves the BaseElement / BaseLayoutId /// inheritance chain. /// public class ChatLayoutConformanceTests { private static (uint, int, int) NoTex(uint _) => (0, 0, 0); private static ElementInfo? Find(ElementInfo n, uint id) { if (n.Id == id) return n; foreach (var c in n.Children) { var f = Find(c, id); if (f is not null) return f; } return null; } [Fact] public void ChatFixture_ResolvesKnownElements() { var root = FixtureLoader.LoadChatInfos(); Assert.NotNull(Find(root, 0x10000011u)); // transcript Assert.NotNull(Find(root, 0x10000016u)); // input Assert.NotNull(Find(root, 0x10000012u)); // scrollbar track Assert.NotNull(Find(root, 0x10000014u)); // channel menu Assert.NotNull(Find(root, 0x10000019u)); // send button Assert.NotNull(Find(root, 0x1000046Fu)); // max/min button } [Fact] public void ChatFixture_ResolvedTypes_MatchRetailRegistry() { var root = FixtureLoader.LoadChatInfos(); Assert.Equal(6u, Find(root, 0x10000014u)!.Type); // Menu Assert.Equal(11u, Find(root, 0x10000012u)!.Type); // Scrollbar Assert.Equal(1u, Find(root, 0x10000019u)!.Type); // Button (Send) Assert.Equal(1u, Find(root, 0x1000046Fu)!.Type); // Button (Max/Min) Assert.Equal(12u, Find(root, 0x10000011u)!.Type); // Text/style-prototype (transcript) Assert.Equal(12u, Find(root, 0x10000016u)!.Type); // Text/style-prototype (input) } [Theory] [InlineData(0x10000522u)] [InlineData(0x10000523u)] [InlineData(0x10000524u)] [InlineData(0x10000525u)] public void ChatFixture_ChatWindowIndicatorButtons_ImportFromTheAuthoredTree(uint indicatorId) { // gmMainChatUI::RecvNotice_SetPanelVisibility @0x004CCD80 writes these // four state-mirror buttons for floating chat windows 1-4. CH6a imports // them generically (visible, inert) and leaves wiring their live/pressed // state to CH6b's floating-window slice. var root = FixtureLoader.LoadChatInfos(); Assert.NotNull(Find(root, indicatorId)); } [Theory] [InlineData(0x10000522u)] [InlineData(0x10000523u)] [InlineData(0x10000524u)] [InlineData(0x10000525u)] public void MountedChatWindow_IndicatorButtons_ImportVisibleAndInert(uint indicatorId) { var infos = FixtureLoader.LoadChatInfos(); var layout = LayoutImporter.Build(infos, NoTex, null); var controller = ChatWindowController.Bind( infos, layout, new ChatVM(new ChatLog()), () => NullCommandBus.Instance, new ChatWindowState(), null, null, NoTex); Assert.NotNull(controller); UiElement? indicator = layout.FindElement(indicatorId); Assert.NotNull(indicator); Assert.True(indicator!.Visible); // Inert: CH6a does not wire a click handler (CH6b's job — the // one-directional visibility mirror has no button-press behavior of // its own in retail either, per the research doc §1.4). UiButton // structurally always reports HandlesClick=true (it can receive the // event), but with no OnClick/OnClickAt bound, a click is a no-op. var button = Assert.IsType(indicator); Assert.Null(button.OnClick); Assert.Null(button.OnClickAt); // CH6a/b REJECT-review SHOULD-FIX 3: OnClick==null is the wrong level // to prove "inert" — the fixture's own 0x0B (ToggleBehavior) = true // means a plain press/release would still flip UiButton's internal // Selected mirror even with no OnClick bound. SuppressSelfToggle // (set by ChatWindowController.Bind) is the actual guard; a press // and release must leave Selected unchanged. Assert.True(button.SuppressSelfToggle); bool before = button.Selected; button.OnEvent(new UiEvent(0, button, UiEventType.MouseDown, Data1: 2, Data2: 2)); button.OnEvent(new UiEvent(0, button, UiEventType.MouseUp, Data1: 2, Data2: 2)); Assert.Equal(before, button.Selected); } [Theory] [InlineData(0x10000693u)] [InlineData(0x10000694u)] [InlineData(0x10000695u)] [InlineData(0x10000696u)] [InlineData(0x10000697u)] [InlineData(0x10000698u)] [InlineData(0x10000699u)] [InlineData(0x1000069Au)] public void MountedChatWindow_LockedTwinBorderArt_DefaultsHidden(uint lockedTwinId) { // Register row AP-185: CH6a shows only the live (unlocked) grip/dragbar // border-art set by default, matching UiRoot.UiLocked's own false // default and avoiding a double-rendered border. var infos = FixtureLoader.LoadChatInfos(); var layout = LayoutImporter.Build(infos, NoTex, null); var controller = ChatWindowController.Bind( infos, layout, new ChatVM(new ChatLog()), () => NullCommandBus.Instance, new ChatWindowState(), null, null, NoTex); Assert.NotNull(controller); UiElement? twin = layout.FindElement(lockedTwinId); Assert.NotNull(twin); Assert.False(twin!.Visible); } [Fact] public void ChatFixture_BuildsSelectableTranscriptAndEditableInputInPlace() { var layout = FixtureLoader.LoadChat(); var transcript = Assert.IsType(layout.FindElement(0x10000011u)); var input = Assert.IsType(layout.FindElement(0x10000016u)); Assert.True(transcript.Selectable); Assert.True(input.Selectable); Assert.True(input.OneLine); Assert.Equal(0x10000016u, input.DatElementId); } [Fact] public void ChatFixture_ScrollbarImportsInheritedMediaRoles() { var layout = FixtureLoader.LoadChat(); var scrollbar = Assert.IsType(layout.FindElement(0x10000012u)); Assert.Equal(0x06004C5Fu, scrollbar.TrackSprite); Assert.Equal(0x06004C60u, scrollbar.ThumbTopSprite); Assert.Equal(0x06004C63u, scrollbar.ThumbSprite); Assert.Equal(0x06004C66u, scrollbar.ThumbBotSprite); Assert.Equal(0x06004C69u, scrollbar.UpSprite); Assert.Equal(0x06004C6Cu, scrollbar.DownSprite); } [Fact] public void ChatFixture_WindowRootCarriesRetailHeightConstraints() { var root = FixtureLoader.LoadChatInfos(); var window = Find(root, 0x10000600u)!; Assert.True(window.TryGetEffectiveInteger(0x3Eu, out int minHeight)); Assert.True(window.TryGetEffectiveInteger(0x3Cu, out int maxHeight)); Assert.Equal(100, minHeight); Assert.Equal(2000, maxHeight); } [Fact] public void ChatFixture_WindowRootCarriesRetailWidthConstraints() { var root = FixtureLoader.LoadChatInfos(); var window = Find(root, 0x10000600u)!; Assert.True(window.TryGetEffectiveInteger(0x3Fu, out int minWidth)); Assert.True(window.TryGetEffectiveInteger(0x3Du, out int maxWidth)); Assert.Equal(300, minWidth); Assert.Equal(2000, maxWidth); } [Fact] public void ChatFixture_RootIsAuthored410x100_NoCropNeeded() { // Campaign CH slice CH6a: 0x2100006F's window root is already the exact // mounted extent — the 490px content-width crop that the wrong // 0x21000006 import needed no longer applies to anything. var root = FixtureLoader.LoadChatInfos(); Assert.Equal(410f, root.Width); Assert.Equal(100f, root.Height); } [Fact] public void ChatFixture_DockedRoot_ReflowsContentAcrossMountedWidth() { var infos = FixtureLoader.LoadChatInfos(); var layout = LayoutImporter.Build(infos, NoTex, null); var controller = ChatWindowController.Bind( infos, layout, new ChatVM(new ChatLog()), () => NullCommandBus.Instance, new ChatWindowState(), null, null, NoTex); Assert.NotNull(controller); // No crop-then-rebase dance needed: the root's OWN authored extent // (410x100) already matches the design width children's imported // LayoutPolicy captured, so growing it directly exercises the same // raw-edge reflow the crop workaround needed a rebase step for. var root = controller!.Root; Assert.Equal(410f, root.Width); root.Width = 615; // Both panels are authored with a 5px margin to BOTH the left and right // window edges (X=5, right-edge width = 410-(5+400) = 5) — a stretch // grows the panel by the delta MINUS both margins: 615 - 5 - 5 = 605. var transcriptPanel = layout.FindElement(0x10000010u)!; var inputBar = layout.FindElement(0x10000013u)!; transcriptPanel.ApplyAnchor(root.Width, root.Height); inputBar.ApplyAnchor(root.Width, root.Height); Assert.Equal(605f, transcriptPanel.Width); Assert.Equal(605f, inputBar.Width); } [Fact] public void ChatFixture_ChannelCaptionWidth_SurvivesImportedLayoutPass() { var infos = FixtureLoader.LoadChatInfos(); var layout = LayoutImporter.Build(infos, NoTex, null); var controller = ChatWindowController.Bind( infos, layout, new ChatVM(new ChatLog()), () => NullCommandBus.Instance, new ChatWindowState(), null, null, NoTex); Assert.NotNull(controller); controller!.Menu.OnSelect!.Invoke(ChatChannelKind.General); float fittedWidth = controller.Menu.Width; controller.Menu.ApplyAnchor(controller.Menu.Parent!.Width, controller.Menu.Parent.Height); Assert.True(fittedWidth > 46f); Assert.Equal(fittedWidth, controller.Menu.Width); Assert.Equal(controller.Menu.Left + fittedWidth, controller.Input.Left); } [Fact] public void ChatMaximize_ResizesOuterFrameByHalfParent_AndRestores() { var infos = FixtureLoader.LoadChatInfos(); var layout = LayoutImporter.Build(infos, NoTex, null); var controller = ChatWindowController.Bind( infos, layout, new ChatVM(new ChatLog()), () => NullCommandBus.Instance, new ChatWindowState(), null, null, NoTex)!; var root = new UiRoot { Width = 800, Height = 600 }; // Campaign CH slice CH6a: 0x2100006F's own border art IS the window // chrome — Chrome=Imported, no content-width crop, no MinWidth override // (the DAT's own 0x3D/0x3F=2000/300 govern). Frame == content, so the // maximize math below operates directly on the authored 410x100 extent // with the DAT's real minH=100/maxH=2000 (not the wrong layout's 360). RetailWindowHandle handle = RetailWindowFrame.Mount( root, controller.Root, NoTex, new RetailWindowFrame.Options { WindowName = WindowNames.Chat, Chrome = RetailWindowChrome.Imported, Left = 12f, Top = 390f, DatConstraintSource = controller.DatWindowInfo, }); controller.AttachWindow(handle); var maxMin = Assert.IsType(layout.FindElement(0x1000046Fu)); // frame.Height=100, parentHeight=600, expansion=300 → targetHeight = // clamp(min(400,600),100,2000) = 400. growUp because Top(390)+400>600. // targetTop = max(0, 390-(400-100)) = 90. Lower edge fixed at 490. maxMin.OnClick!(); Assert.True(controller.IsMaximized); Assert.Equal(90f, handle.Top); Assert.Equal(400f, handle.Height); Assert.Equal(490f, handle.Top + handle.Height); // lower edge stays fixed while growing upward Assert.Equal(RetailUiStateIds.Maximized, maxMin.ActiveRetailStateId); maxMin.OnClick!(); Assert.False(controller.IsMaximized); Assert.Equal(390f, handle.Top); Assert.Equal(100f, handle.Height); Assert.Equal(RetailUiStateIds.Minimized, maxMin.ActiveRetailStateId); } [Theory] [InlineData(0x1000069Bu, UiResizeGrip.Border.UpperLeft)] [InlineData(0x1000069Du, UiResizeGrip.Border.UpperRight)] [InlineData(0x1000069Fu, UiResizeGrip.Border.LowerLeft)] [InlineData(0x100006A1u, UiResizeGrip.Border.LowerRight)] public void MountedChatWindow_CornerGrip_ImportsWithCorrectBorderLocation( uint elementId, UiResizeGrip.Border expected) { // Campaign CH slice CH6a, user-gate round 2 item 6: pins that the 4 // corner grips from 0x2100006F import with the RIGHT BorderLocation — // the prerequisite for the reported "no diagonal cursor" / "can't grow // in Y from the bottom-right corner" symptoms to be fixed at all. var layout = FixtureLoader.LoadChat(); var grip = Assert.IsType(layout.FindElement(elementId)); Assert.Equal(expected, grip.BorderLocation); } [Fact] public void MountedChatWindow_TopStrip_IsAMoveHandleNotAGrip() { // 0x1000069C (the plain top edge, between the two top corners) is a // Type-2 Dragbar in the real DAT, not a Resizebar — retail's main chat // window has no title bar, so the top strip moves the window while its // two corners (above) resize it. var layout = FixtureLoader.LoadChat(); var topStrip = layout.FindElement(0x1000069Cu); Assert.NotNull(topStrip); Assert.IsNotType(topStrip); Assert.True(topStrip!.WindowMoveHandle); } [Theory] [InlineData(0x1000069Bu)] // TL corner [InlineData(0x1000069Du)] // TR corner [InlineData(0x1000069Eu)] // left edge [InlineData(0x1000069Fu)] // BL corner [InlineData(0x100006A0u)] // bottom edge [InlineData(0x100006A1u)] // BR corner [InlineData(0x100006A2u)] // right edge public void MountedChatWindow_LiveGrip_ResolvesNonZeroSprite(uint elementId) { // CH6a/b REJECT-review BLOCKER 1: pre-fix, UiResizeGrip drew nothing — // it was constructed without its ElementInfo/resolve pair, so all seven // live Type-9 grips rendered NOTHING even though the fixture proves // every one of them carries real authored art (the 0x06006129 family). // This is the regression guard: every live grip must resolve a non-zero // DirectState sprite id, not just decode the right BorderLocation. var layout = FixtureLoader.LoadChat(); var grip = Assert.IsType(layout.FindElement(elementId)); Assert.NotEqual(0u, grip.SpriteFile); } private sealed class NullGpuFrameSource : ICurrentGpuFrameSource { public IGpuFrame? CurrentFrame => null; } [Theory] [InlineData(0x1000069Bu)] // TL corner [InlineData(0x1000069Du)] // TR corner [InlineData(0x1000069Eu)] // left edge [InlineData(0x1000069Fu)] // BL corner [InlineData(0x100006A0u)] // bottom edge [InlineData(0x100006A1u)] // BR corner [InlineData(0x100006A2u)] // right edge public void MountedChatWindow_LiveGrip_ActuallyEmitsASpriteDraw_NotJustResolvesSpriteFile(uint elementId) { // CH6a/b re-review rider: MountedChatWindow_LiveGrip_ResolvesNonZeroSprite // (above) only proves the ElementInfo carries a non-zero DirectState // sprite id — it never calls OnDraw, so a media-less regression (a grip // constructed WITHOUT its resolve delegate, or one whose resolve always // returns a zero handle/dimension — exactly the CH6a/b BLOCKER 1 bug) // would still pass it. This drives the grip through a REAL // UiRenderContext over a REAL TextRenderer (backed by the in-memory // RecordingGpuDevice test double, no live GPU) and asserts the draw // call chain actually queued sprite geometry for that grip's texture. var infos = FixtureLoader.LoadChatInfos(); // Distinct from FixtureLoader's own null-returning resolver: echoes the // sprite id as a nonzero fake texture handle with nonzero dimensions, // so UiResizeGrip.OnDraw's `tex == 0 || tw == 0 || th == 0` guard does // not short-circuit before reaching ctx.DrawSprite. var layout = LayoutImporter.Build(infos, id => (id, 8, 8), null); var grip = Assert.IsType(layout.FindElement(elementId)); Assert.NotEqual(0u, grip.SpriteFile); Assert.True(grip.Visible); var device = new RecordingGpuDevice(); var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused"); renderer.Begin(new Vector2(800f, 600f)); var ctx = new UiRenderContext(renderer, new Vector2(800f, 600f)); grip.DrawSelfAndChildren(ctx); var seg = Assert.Single( renderer.DebugSpriteSegments, s => s.Texture == grip.SpriteFile); Assert.True(seg.VertexCount > 0); } [Fact] public void MountedChatWindow_BottomRightGrip_GrowsBothAxes_NotOnlyShrinks() { // The literal user-gate round 2 report: "the window cannot grow in the // Y axis when dragging from the bottom-right corner." Exercises the // FULL production path (ChatWindowController.Bind + RetailWindowFrame.Mount // with the real Chrome=Imported options and the real DAT min/max // constraints from 0x2100006F, minH=100/maxH=2000/minW=300/maxW=2000) // rather than just the pure ResizeRect math. var infos = FixtureLoader.LoadChatInfos(); var layout = LayoutImporter.Build(infos, NoTex, null); var controller = ChatWindowController.Bind( infos, layout, new ChatVM(new ChatLog()), () => NullCommandBus.Instance, new ChatWindowState(), null, null, NoTex)!; var root = new UiRoot { Width = 1600, Height = 1200 }; RetailWindowHandle handle = RetailWindowFrame.Mount( root, controller.Root, NoTex, new RetailWindowFrame.Options { WindowName = WindowNames.Chat, Chrome = RetailWindowChrome.Imported, Left = 10f, Top = 440f, DatConstraintSource = controller.DatWindowInfo, }); controller.AttachWindow(handle); var brGrip = Assert.IsType(layout.FindElement(0x100006A1u)); Assert.Equal(UiResizeGrip.Border.LowerRight, brGrip.BorderLocation); var gs = brGrip.ScreenPosition; int pressX = (int)(gs.X + 2), pressY = (int)(gs.Y + 2); // Shrink first (this direction was never in question per the report). // Height is already AT the DAT's authored minimum (minH=100 == the // authored 100px height), so it clamps rather than shrinking further — // width has headroom (minW=300 < 410) and does shrink. root.OnMouseDown(UiMouseButton.Left, pressX, pressY); root.OnMouseMove(pressX - 20, pressY - 20); root.OnMouseUp(UiMouseButton.Left, pressX - 20, pressY - 20); Assert.Equal(390f, handle.Width); Assert.Equal(100f, handle.Height); // Now grow from the shrunken state — this is the reported-broken direction. var brGripAfterShrink = Assert.IsType(layout.FindElement(0x100006A1u)); var gs2 = brGripAfterShrink.ScreenPosition; int pressX2 = (int)(gs2.X + 2), pressY2 = (int)(gs2.Y + 2); root.OnMouseDown(UiMouseButton.Left, pressX2, pressY2); root.OnMouseMove(pressX2 + 70, pressY2 + 70); root.OnMouseUp(UiMouseButton.Left, pressX2 + 70, pressY2 + 70); Assert.Equal(460f, handle.Width); Assert.Equal(170f, handle.Height); } [Fact] public void MountedChatScrollbar_ButtonsAndThumbDragDriveTranscriptModel() { var infos = FixtureLoader.LoadChatInfos(); var layout = LayoutImporter.Build(infos, NoTex, null); var controller = ChatWindowController.Bind( infos, layout, new ChatVM(new ChatLog()), () => NullCommandBus.Instance, new ChatWindowState(), null, null, NoTex)!; var root = new UiRoot { Width = 800, Height = 600 }; RetailWindowHandle handle = RetailWindowFrame.Mount( root, controller.Root, NoTex, new RetailWindowFrame.Options { WindowName = WindowNames.Chat, Chrome = RetailWindowChrome.Imported, Left = 10f, Top = 440f, DatConstraintSource = controller.DatWindowInfo, }); controller.AttachWindow(handle); UiScrollbar bar = controller.Scrollbar; UiScrollable scroll = controller.Transcript.Scroll; scroll.LineHeight = 16; scroll.SetExtents(contentHeight: 400, viewHeight: 50, preserveEnd: true); Assert.Equal(350, scroll.ScrollY); var screen = bar.ScreenPosition; int centerX = (int)(screen.X + bar.Width / 2f); // Top/decrement button. int upY = (int)(screen.Y + 8f); root.OnMouseDown(UiMouseButton.Left, centerX, upY); root.OnMouseUp(UiMouseButton.Left, centerX, upY); Assert.Equal(334, scroll.ScrollY); // Re-layout must retain the position chosen through the scrollbar. scroll.SetExtents(contentHeight: 400, viewHeight: 50, preserveEnd: true); Assert.Equal(334, scroll.ScrollY); // Drag the thumb toward the top of the track. float trackTop = 16f; float trackLength = bar.Height - 32f; var (thumbY, thumbHeight) = UiScrollbar.ThumbRect(scroll, trackTop, trackLength); int dragStartY = (int)(screen.Y + thumbY + thumbHeight / 2f); int dragEndY = (int)(screen.Y + trackTop + thumbHeight / 2f); root.OnMouseDown(UiMouseButton.Left, centerX, dragStartY); root.OnMouseMove(centerX, dragEndY); root.OnMouseUp(UiMouseButton.Left, centerX, dragEndY); Assert.True(scroll.ScrollY < 334); int draggedPosition = scroll.ScrollY; scroll.SetExtents(contentHeight: 400, viewHeight: 50, preserveEnd: true); Assert.Equal(draggedPosition, scroll.ScrollY); } }