Swap ChatWindowController's imported main-chat LayoutDesc from the wrong 0x21000006 (an unrelated layout whose root and 800px resize bar appear nowhere in the EoR gameplay UI) to retail's ACTUAL main chat window, 0x2100006F (window root 0x10000600, authored 410x100 — confirmed by a direct DAT dump, found in dats.Local not dats.Portal). Every downstream compensation that existed only to paper over the wrong import is deleted: the hand-cropped 490px content width, the dropped 800px resize bar, the 9px transcript patch, the orphan-sibling pruning, the max/min-vs-scrollbar overlap shift, and the scrollbar top-reclaim. The window now mounts with RetailWindowChrome.Imported (0x2100006F's own 8 border/corner elements are its complete chrome) instead of the universal nine-slice wrapper. LayoutImporter/DatWidgetFactory gain a Type-9 (UIElement_Resizebar) case: UiResizeGrip decodes retail's exact four-bool BorderLocation algorithm (0x2A=bottom/0x2B=left/0x2C=right/0x2D=top, UIElement_Resizebar::StartMouseResizing @0x0046B7E0) into a ResizeEdges bitmask. A direct DAT dump established the true shape: only 7 of the 8 grip-position ids are Type 9 — the straight top-EDGE strip (0x1000069C) is a Type-2 Dragbar (move handle), not a Resizebar, because the main window has no title bar. UiRoot now gives a directly-hit grip's own edges priority over its generic proximity heuristic, and a directly-hit move handle the same priority over ambient proximity — so the plain top strip moves the window while its two corner grips resize it including the Y axis, and all 4 edges + 4 corners work everywhere else. This also fixes the reported "no diagonal cursor at corners" (CursorFeedbackController's existing RetailCursorCatalog cursor ids already matched the DAT exactly; they just never received a genuine diagonal edge combination) and "cannot grow in Y from the bottom-right corner" (the old NineSlice+crop mount's indirection is gone; the Imported mount uses the DAT's real minH=100/maxH=2000/minW=300/maxW=2000 directly). The 8 cosmetic "_Locked" border-art twins default hidden (register row AP-185 — retail's UiLocked-driven art swap between the two skins is not ported; UiRoot.UiLocked continues to gate the underlying interaction correctly either way). The 4 chat-window-1..4 indicator buttons import generically (visible, inert) for CH6b to wire. The two hand-drawn translucent-black tints on the transcript/input are removed now that their parent panels draw their own authored background sprites. Filed #366 (chat window's new-unseen-text indicator 0x1000048C is swallowed by UiText.ConsumesDatChildren, pre-existing and out of scope). Corrected the research doc's "all eight grips" claim against the direct DAT dump. Full Release suite: 12,317 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
451 lines
18 KiB
C#
451 lines
18 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, 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);
|
|
}
|
|
|
|
[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, 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,
|
|
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,
|
|
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,
|
|
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);
|
|
}
|
|
|
|
[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,
|
|
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,
|
|
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);
|
|
}
|
|
}
|