Applies docs/research/2026-08-10-ch6ab-review-findings.md in full:
- BLOCKER 1: UiResizeGrip now carries its ElementInfo/resolve pair and
draws its own authored DirectState media (a synthetic parameterless
grip still draws nothing, preserving existing resize-drag tests).
DatWidgetFactory.BuildResizeGrip threads resolve through. All seven
live grips on the main chat window now resolve a non-zero sprite,
restoring the visible borders/corners CH6a silently dropped.
- SHOULD-FIX 2: ChatWindowState gains BroadcastTargetWindow, a sentinel
distinct from every real window id (0-4), fixing the bug where the
main window's explicit-addressing branch coincided with the broadcast
check (both were literal 0). SetFilter's main-window no-op is dropped
— the main window's filter is now genuinely settable. ChatWindowController
.Bind takes a ChatWindowState (the same canonical instance the floating
windows already share) and GetTranscriptLines builds a real accept
predicate instead of accept:null. Verified safe: ClientLocal (0x1A)
never reaches ChatLog (AddText routes it to the SpewBox and returns),
so nothing observable regresses.
- SHOULD-FIX 3: UiButton.SuppressSelfToggle stops the four chat-window
indicator buttons (DAT property 0x0B=true, no retail click handler)
from flipping their own Selected mirror on a stray click.
- SHOULD-FIX 4: generated and committed chat_floaty_2100005b.json from
the real installed dats; added the permanent RetailLayoutFixtureGenerator
entry. All three flagged FloatingChatWindowController assumptions
(input field, title bar, close button) are confirmed correct against
real data — no controller code changes needed. New finding: unlike the
main window, ALL EIGHT floaty border/corner elements are live Type-9
grips (the floaty's own title bar is its move handle), so a floaty
window resizes from every edge and corner.
- SHOULD-FIX 5: register row AP-189 documents the shared-500-entry/
200-line-tail vs retail's per-window 10,000-line scrollback depth gap.
- NITs 1-5: documented the filter-persistence-only-on-/saveautoui
asymmetry and the reconnect-preserves-filters intent; corrected the
research doc's modifier-mask mislabel and the "ONLY function" false
superlative; moved WrapText off ChatWindowController onto
ChatTranscriptRenderer, closing the circular dependency.
Full Release suite: 12,420 passed / 4 skipped / 0 failed (baseline
12,392/4/0 at 22020ef2; net +28 tests, zero regressions).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
489 lines
20 KiB
C#
489 lines
20 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Dat-free conformance tests for the committed chat_2100006f.json golden fixture —
|
|
/// retail's ACTUAL main chat window (LayoutDesc <c>0x2100006F</c>, window root
|
|
/// <c>0x10000600</c>, authored 410x100; Campaign CH slice CH6a). Verifies that
|
|
/// LayoutImporter.ImportInfos correctly resolves the BaseElement / BaseLayoutId
|
|
/// inheritance chain.
|
|
/// </summary>
|
|
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<UiButton>(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<UiText>(layout.FindElement(0x10000011u));
|
|
var input = Assert.IsType<UiField>(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<UiScrollbar>(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<UiButton>(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<UiResizeGrip>(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<UiResizeGrip>(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<UiResizeGrip>(layout.FindElement(elementId));
|
|
Assert.NotEqual(0u, grip.SpriteFile);
|
|
}
|
|
|
|
[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<UiResizeGrip>(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<UiResizeGrip>(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);
|
|
}
|
|
}
|