feat(ui): port retained widget foundations
This commit is contained in:
parent
44f9ec13d9
commit
d825572e31
44 changed files with 84813 additions and 292 deletions
|
|
@ -1528,10 +1528,12 @@ public class CharacterStatControllerTests
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>Manufacture a minimal UiButton with a fake ElementInfo (no dat sprites).</summary>
|
||||
/// <summary>Manufacture a minimal UiButton with retail normal/ghosted states.</summary>
|
||||
private static UiButton MakeButton(uint id = 0u)
|
||||
{
|
||||
var info = new ElementInfo { Id = id, Type = 1 };
|
||||
info.StateMedia["Normal"] = (1u, 1);
|
||||
info.StateMedia["Ghosted"] = (2u, 1);
|
||||
return new UiButton(info, static _ => (0u, 0, 0));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
|
@ -43,4 +44,18 @@ public class ChatLayoutConformanceTests
|
|||
Assert.Equal(12u, Find(root, 0x10000011u)!.Type); // Text/style-prototype (transcript)
|
||||
Assert.Equal(12u, Find(root, 0x10000016u)!.Type); // Text/style-prototype (input)
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,8 @@ namespace AcDream.App.Tests.UI.Layout;
|
|||
/// real chat layout hierarchy (root → transcript panel + input bar as Type-3
|
||||
/// containers, with Type-12 children for transcript + input, plus a Type-3 track
|
||||
/// and menu), call <see cref="LayoutImporter.Build"/> to get the widget tree
|
||||
/// (Type-12 children skipped, Type-3 parents created), then call
|
||||
/// <see cref="ChatWindowController.Bind"/> which reads rects from the info tree
|
||||
/// and places behavioral widgets under the parent containers.
|
||||
/// (Type-12 children become property-driven text/field widgets), then call
|
||||
/// <see cref="ChatWindowController.Bind"/> which binds those widgets in place.
|
||||
/// </summary>
|
||||
public class ChatWindowControllerTests
|
||||
{
|
||||
|
|
@ -42,7 +41,7 @@ public class ChatWindowControllerTests
|
|||
/// track (Type-3) [0x10000012] ← Type-3 in test (not Type-11); Bind skips scrollbar bind
|
||||
/// inputBar (Type-3) [0x10000013]
|
||||
/// menu (Type-6) [0x10000014]
|
||||
/// input (Type-12, no media) [0x10000016] ← built as UiText by factory; Bind removes + replaces with UiField
|
||||
/// input (Type-12, Editable+Selectable) [0x10000016] ← built as UiField
|
||||
/// send (Type-3) [0x10000019]
|
||||
/// maxmin (Type-3) [0x1000046F]
|
||||
/// </summary>
|
||||
|
|
@ -71,9 +70,26 @@ public class ChatWindowControllerTests
|
|||
};
|
||||
var inputNode = new ElementInfo
|
||||
{
|
||||
Id = 0x10000016u, Type = 12, // Type-12, no media → skipped by factory
|
||||
Id = 0x10000016u, Type = 12,
|
||||
X = 46, Y = 0, Width = 398, Height = 17,
|
||||
};
|
||||
var inputState = new UiStateInfo { Id = UiStateInfo.DirectStateId };
|
||||
inputState.Properties.Values[0x16u] = new UiPropertyValue
|
||||
{
|
||||
Kind = UiPropertyKind.Bool,
|
||||
BoolValue = true,
|
||||
};
|
||||
inputState.Properties.Values[0x20u] = new UiPropertyValue
|
||||
{
|
||||
Kind = UiPropertyKind.Bool,
|
||||
BoolValue = true,
|
||||
};
|
||||
inputState.Properties.Values[0x27u] = new UiPropertyValue
|
||||
{
|
||||
Kind = UiPropertyKind.Bool,
|
||||
BoolValue = true,
|
||||
};
|
||||
inputNode.States[UiStateInfo.DirectStateId] = inputState;
|
||||
var sendNode = new ElementInfo
|
||||
{
|
||||
Id = 0x10000019u, Type = 3, X = 444, Y = 0, Width = 46, Height = 17,
|
||||
|
|
|
|||
|
|
@ -42,7 +42,42 @@ public class DatWidgetFactoryTests
|
|||
public void Type12_Text_MakesUiText()
|
||||
{
|
||||
var e = DatWidgetFactory.Create(new ElementInfo { Type = 12, Width = 100, Height = 40 }, NoTex, null);
|
||||
Assert.IsType<UiText>(e);
|
||||
var text = Assert.IsType<UiText>(e);
|
||||
Assert.False(text.Selectable);
|
||||
Assert.True(text.ClickThrough);
|
||||
Assert.False(text.AcceptsFocus);
|
||||
Assert.False(text.CapturesPointerDrag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Type12_EditableProperty_MakesUiFieldInPlace()
|
||||
{
|
||||
var info = TextInfo(
|
||||
(0x16u, Bool(true)),
|
||||
(0x20u, Bool(true)),
|
||||
(0x27u, Bool(true)),
|
||||
(0x1Eu, Integer(80)));
|
||||
|
||||
var field = Assert.IsType<UiField>(DatWidgetFactory.Create(info, NoTex, null));
|
||||
|
||||
Assert.Equal(info.Id, field.ElementId);
|
||||
Assert.True(field.OneLine);
|
||||
Assert.True(field.Selectable);
|
||||
Assert.Equal(80, field.MaxCharacters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Type12_SelectableProperty_MakesSelectableUiText()
|
||||
{
|
||||
var text = Assert.IsType<UiText>(DatWidgetFactory.Create(
|
||||
TextInfo((0x27u, Bool(true))),
|
||||
NoTex,
|
||||
null));
|
||||
|
||||
Assert.True(text.Selectable);
|
||||
Assert.False(text.ClickThrough);
|
||||
Assert.True(text.AcceptsFocus);
|
||||
Assert.True(text.CapturesPointerDrag);
|
||||
}
|
||||
|
||||
// ── Test 4: Rect + anchors set from ElementInfo ───────────────────────────
|
||||
|
|
@ -74,6 +109,46 @@ public class DatWidgetFactoryTests
|
|||
Assert.Equal(AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right, e.Anchors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImportedDescendant_PreservesExactRawEdgePolicy()
|
||||
{
|
||||
var info = new ElementInfo
|
||||
{
|
||||
Type = 3,
|
||||
X = 10,
|
||||
Y = 20,
|
||||
Width = 30,
|
||||
Height = 40,
|
||||
Left = 4,
|
||||
Top = 3,
|
||||
Right = 1,
|
||||
Bottom = 0,
|
||||
OriginalParentWidth = 100,
|
||||
OriginalParentHeight = 200,
|
||||
HasOriginalParentSize = true,
|
||||
};
|
||||
|
||||
var element = DatWidgetFactory.Create(info, NoTex, null)!;
|
||||
var policy = Assert.IsType<UiLayoutPolicy>(element.LayoutPolicy);
|
||||
|
||||
Assert.Equal(4u, policy.LeftMode);
|
||||
Assert.Equal(3u, policy.TopMode);
|
||||
Assert.Equal(1u, policy.RightMode);
|
||||
Assert.Equal(0u, policy.BottomMode);
|
||||
Assert.Equal(new UiPixelRect(0, 0, 99, 199), policy.OriginalParent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImportedRoot_HasNoRawEdgePolicy()
|
||||
{
|
||||
var element = DatWidgetFactory.Create(
|
||||
new ElementInfo { Type = 3, Width = 100, Height = 200 },
|
||||
NoTex,
|
||||
null)!;
|
||||
|
||||
Assert.Null(element.LayoutPolicy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_PropagatesStateCursors()
|
||||
{
|
||||
|
|
@ -339,6 +414,22 @@ public class DatWidgetFactoryTests
|
|||
Assert.Equal(gold, t.DefaultColor);
|
||||
}
|
||||
|
||||
private static ElementInfo TextInfo(params (uint Id, UiPropertyValue Value)[] properties)
|
||||
{
|
||||
var info = new ElementInfo { Id = 0x10000016u, Type = 12, Width = 100, Height = 20 };
|
||||
var state = new UiStateInfo { Id = UiStateInfo.DirectStateId };
|
||||
foreach (var property in properties)
|
||||
state.Properties.Values[property.Id] = property.Value;
|
||||
info.States[UiStateInfo.DirectStateId] = state;
|
||||
return info;
|
||||
}
|
||||
|
||||
private static UiPropertyValue Bool(bool value)
|
||||
=> new() { Kind = UiPropertyKind.Bool, BoolValue = value };
|
||||
|
||||
private static UiPropertyValue Integer(int value)
|
||||
=> new() { Kind = UiPropertyKind.Integer, IntegerValue = value };
|
||||
|
||||
/// <summary>
|
||||
/// When ElementInfo.FontColor is null (dat carried no 0x1B), DefaultColor must
|
||||
/// stay at white (Vector4.One) — the backward-compatible default.
|
||||
|
|
|
|||
|
|
@ -65,6 +65,30 @@ public static class FixtureLoader
|
|||
public static AcDream.App.UI.Layout.ElementInfo LoadRadarInfos()
|
||||
=> LoadInfos("radar_21000074.json");
|
||||
|
||||
public static ImportedLayout LoadToolbar()
|
||||
=> LayoutImporter.Build(LoadToolbarInfos(), _ => (0u, 0, 0), null);
|
||||
|
||||
public static ElementInfo LoadToolbarInfos()
|
||||
=> LoadInfos("toolbar_21000016.json");
|
||||
|
||||
public static ImportedLayout LoadInventory()
|
||||
=> LayoutImporter.Build(LoadInventoryInfos(), _ => (0u, 0, 0), null);
|
||||
|
||||
public static ElementInfo LoadInventoryInfos()
|
||||
=> LoadInfos("inventory_21000023.json");
|
||||
|
||||
public static ImportedLayout LoadPaperdoll()
|
||||
=> LayoutImporter.Build(LoadPaperdollInfos(), _ => (0u, 0, 0), null);
|
||||
|
||||
public static ElementInfo LoadPaperdollInfos()
|
||||
=> LoadInfos("paperdoll_21000024.json");
|
||||
|
||||
public static ImportedLayout LoadCharacter()
|
||||
=> LayoutImporter.Build(LoadCharacterInfos(), _ => (0u, 0, 0), null);
|
||||
|
||||
public static ElementInfo LoadCharacterInfos()
|
||||
=> LoadInfos("character_2100002E.json");
|
||||
|
||||
// ── Shared loader ────────────────────────────────────────────────────────
|
||||
|
||||
private static AcDream.App.UI.Layout.ElementInfo LoadInfos(string fileName)
|
||||
|
|
|
|||
|
|
@ -139,6 +139,36 @@ public class LayoutConformanceTests
|
|||
Assert.Contains(0x40000000u, fontDids);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VitalsBinding_ReusesAuthoredTextNodesWithoutDuplicates()
|
||||
{
|
||||
var layout = FixtureLoader.LoadVitals();
|
||||
VitalsController.Bind(
|
||||
layout,
|
||||
() => 1f,
|
||||
() => 1f,
|
||||
() => 1f,
|
||||
() => "100/100",
|
||||
() => "90/100",
|
||||
() => "80/100");
|
||||
|
||||
(uint MeterId, uint TextId)[] cases =
|
||||
[
|
||||
(VitalsController.Health, VitalsController.HealthText),
|
||||
(VitalsController.Stamina, VitalsController.StaminaText),
|
||||
(VitalsController.Mana, VitalsController.ManaText),
|
||||
];
|
||||
|
||||
foreach (var (meterId, textId) in cases)
|
||||
{
|
||||
var meter = Assert.IsType<UiMeter>(layout.FindElement(meterId));
|
||||
var text = Assert.IsType<UiText>(layout.FindElement(textId));
|
||||
Assert.Same(meter, text.Parent);
|
||||
Assert.Single(meter.Children, child => child == text);
|
||||
Assert.False(text.Selectable);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectFontDids(ElementInfo node, System.Collections.Generic.List<uint> acc)
|
||||
{
|
||||
if (node.FontDid != 0) acc.Add(node.FontDid);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
using AcDream.App.UI.Layout;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
public sealed class RetailFixtureConformanceTests
|
||||
{
|
||||
[Theory]
|
||||
[MemberData(nameof(LayoutCases))]
|
||||
public void ProductionFixture_PreservesRootAndCanonicalStates(
|
||||
Func<ElementInfo> load,
|
||||
uint rootId,
|
||||
uint rootType,
|
||||
float width,
|
||||
float height,
|
||||
int minimumNodeCount)
|
||||
{
|
||||
var root = load();
|
||||
|
||||
Assert.Equal(rootId, root.Id);
|
||||
Assert.Equal(rootType, root.Type);
|
||||
Assert.Equal(width, root.Width);
|
||||
Assert.Equal(height, root.Height);
|
||||
Assert.True(CountNodes(root) >= minimumNodeCount);
|
||||
Assert.Contains(UiStateInfo.DirectStateId, root.States.Keys);
|
||||
Assert.Contains(Enumerate(root), element => element.States.Count > 1);
|
||||
Assert.Contains(Enumerate(root), element =>
|
||||
element.States.Values.Any(state => state.Properties.Values.Count > 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToolbarFixture_ContainsObjectButtonsAndRetailButtonStates()
|
||||
{
|
||||
var root = FixtureLoader.LoadToolbarInfos();
|
||||
|
||||
var character = Find(root, 0x10000199u);
|
||||
var inventory = Find(root, 0x100001B1u);
|
||||
|
||||
Assert.NotNull(character);
|
||||
Assert.NotNull(inventory);
|
||||
Assert.Contains(1u, character.States.Keys); // Normal
|
||||
Assert.Contains(3u, character.States.Keys); // Normal_pressed
|
||||
Assert.Contains(6u, character.States.Keys); // Highlight
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InventoryFixture_ContainsMountedBackpackAndPaperdollContent()
|
||||
{
|
||||
var root = FixtureLoader.LoadInventoryInfos();
|
||||
|
||||
Assert.NotNull(Find(root, InventoryController.ContentsGridId));
|
||||
Assert.NotNull(Find(root, InventoryController.ContainerListId));
|
||||
Assert.NotNull(Find(root, InventoryController.BurdenMeterId));
|
||||
Assert.NotNull(Find(root, 0x100001D5u)); // mounted paperdoll viewport
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CharacterFixture_ContainsAllThreeRuntimePages()
|
||||
{
|
||||
var root = FixtureLoader.LoadCharacterInfos();
|
||||
|
||||
Assert.NotNull(Find(root, CharacterStatController.AttributesPageId));
|
||||
Assert.NotNull(Find(root, CharacterStatController.SkillsPageId));
|
||||
Assert.NotNull(Find(root, CharacterStatController.TitlesPageId));
|
||||
}
|
||||
|
||||
public static TheoryData<Func<ElementInfo>, uint, uint, float, float, int> LayoutCases
|
||||
=> new()
|
||||
{
|
||||
{ FixtureLoader.LoadToolbarInfos, 0x10000191u, 0x10000007u, 300f, 122f, 20 },
|
||||
{ FixtureLoader.LoadInventoryInfos, 0x100001CCu, 0x10000023u, 300f, 362f, 40 },
|
||||
{ FixtureLoader.LoadPaperdollInfos, 0x100001D4u, 0x10000024u, 224f, 214f, 20 },
|
||||
{ FixtureLoader.LoadCharacterInfos, 0x10000227u, 8u, 300f, 600f, 60 },
|
||||
};
|
||||
|
||||
private static int CountNodes(ElementInfo root)
|
||||
=> Enumerate(root).Count();
|
||||
|
||||
private static IEnumerable<ElementInfo> Enumerate(ElementInfo root)
|
||||
{
|
||||
yield return root;
|
||||
foreach (var child in root.Children)
|
||||
foreach (var descendant in Enumerate(child))
|
||||
yield return descendant;
|
||||
}
|
||||
|
||||
private static ElementInfo? Find(ElementInfo root, uint id)
|
||||
=> Enumerate(root).FirstOrDefault(element => element.Id == id);
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using AcDream.App.UI.Layout;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.Options;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Regenerates every committed retail layout fixture from production portal.dat.
|
||||
/// The test is inert unless <c>ACDREAM_REGENERATE_UI_FIXTURES=1</c> is set, keeping
|
||||
/// normal test runs deterministic and dat-independent.
|
||||
/// </summary>
|
||||
public sealed class RetailLayoutFixtureGenerator
|
||||
{
|
||||
private static readonly (uint Id, string FileName)[] Layouts =
|
||||
{
|
||||
(0x21000006u, "chat_21000006.json"),
|
||||
(0x21000016u, "toolbar_21000016.json"),
|
||||
(0x21000023u, "inventory_21000023.json"),
|
||||
(0x21000024u, "paperdoll_21000024.json"),
|
||||
(0x2100002Eu, "character_2100002E.json"),
|
||||
(0x2100006Cu, "vitals_2100006C.json"),
|
||||
(0x21000074u, "radar_21000074.json"),
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void RegenerateAllRetailFixtures_WhenExplicitlyRequested()
|
||||
{
|
||||
if (!string.Equals(
|
||||
Environment.GetEnvironmentVariable("ACDREAM_REGENERATE_UI_FIXTURES"),
|
||||
"1",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var datDir = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
||||
?? Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
"Documents",
|
||||
"Asheron's Call");
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
|
||||
foreach (var (layoutId, fileName) in Layouts)
|
||||
{
|
||||
var info = LayoutImporter.ImportInfos(dats, layoutId);
|
||||
Assert.NotNull(info);
|
||||
var json = JsonSerializer.Serialize(info, new JsonSerializerOptions
|
||||
{
|
||||
IncludeFields = true,
|
||||
WriteIndented = true,
|
||||
});
|
||||
File.WriteAllText(Path.Combine(FixtureDirectory(), fileName), json);
|
||||
}
|
||||
}
|
||||
|
||||
private static string FixtureDirectory([CallerFilePath] string thisFile = "")
|
||||
=> Path.Combine(Path.GetDirectoryName(thisFile)!, "fixtures");
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
|
|
@ -87,4 +88,42 @@ public class UiDatElementTests
|
|||
// 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
138
tests/AcDream.App.Tests/UI/Layout/UiPropertyBagTests.cs
Normal file
138
tests/AcDream.App.Tests/UI/Layout/UiPropertyBagTests.cs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.UI.Layout;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
public sealed class UiPropertyBagTests
|
||||
{
|
||||
[Fact]
|
||||
public void Merge_UsesKeyPresence_ForExplicitFalseAndZero()
|
||||
{
|
||||
var baseBag = new UiPropertyBag();
|
||||
baseBag.Values[0x16u] = Bool(true);
|
||||
baseBag.Values[0x14u] = Enum(3u);
|
||||
|
||||
var derivedBag = new UiPropertyBag();
|
||||
derivedBag.Values[0x16u] = Bool(false);
|
||||
derivedBag.Values[0x14u] = Enum(0u);
|
||||
|
||||
var merged = UiPropertyBag.Merge(baseBag, derivedBag);
|
||||
|
||||
Assert.False(merged.Values[0x16u].BoolValue);
|
||||
Assert.Equal(0ul, merged.Values[0x14u].UnsignedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EffectiveProperty_NamedStateOverridesDirectStateByPresence()
|
||||
{
|
||||
var info = new ElementInfo { DefaultStateId = 1u };
|
||||
info.States[UiStateInfo.DirectStateId] = State(
|
||||
UiStateInfo.DirectStateId,
|
||||
"",
|
||||
(0x16u, Bool(true)));
|
||||
info.States[1u] = State(1u, "Normal", (0x16u, Bool(false)));
|
||||
|
||||
Assert.True(info.TryGetEffectiveProperty(0x16u, out var property));
|
||||
Assert.False(property.BoolValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ElementMerge_ExplicitCenteredStatePropertyOverridesBaseLeft()
|
||||
{
|
||||
var baseInfo = new ElementInfo();
|
||||
baseInfo.States[UiStateInfo.DirectStateId] = State(
|
||||
UiStateInfo.DirectStateId,
|
||||
"",
|
||||
(0x14u, Enum(0u)));
|
||||
|
||||
var derivedInfo = new ElementInfo();
|
||||
derivedInfo.States[UiStateInfo.DirectStateId] = State(
|
||||
UiStateInfo.DirectStateId,
|
||||
"",
|
||||
(0x14u, Enum(1u)));
|
||||
|
||||
var merged = ElementReader.Merge(baseInfo, derivedInfo);
|
||||
|
||||
Assert.Equal(HJustify.Center, merged.HJustify);
|
||||
Assert.Equal(1ul, merged.States[UiStateInfo.DirectStateId]
|
||||
.Properties.Values[0x14u].UnsignedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertProperty_PreservesNestedRetailValues()
|
||||
{
|
||||
var source = new StructBaseProperty { MasterPropertyId = 0xA0u };
|
||||
var array = new ArrayBaseProperty { MasterPropertyId = 0xA1u };
|
||||
array.Value.Add(new IntegerBaseProperty { MasterPropertyId = 0xA2u, Value = -17 });
|
||||
array.Value.Add(new Bitfield64BaseProperty
|
||||
{
|
||||
MasterPropertyId = 0xA3u,
|
||||
Value = 0xFEDCBA9876543210ul,
|
||||
});
|
||||
source.Value[0x55u] = array;
|
||||
source.Value[0x56u] = new VectorBaseProperty
|
||||
{
|
||||
MasterPropertyId = 0xA4u,
|
||||
Value = new Vector3(1.25f, -2.5f, 9.75f),
|
||||
};
|
||||
|
||||
var converted = LayoutImporter.ConvertProperty(source);
|
||||
|
||||
Assert.Equal(UiPropertyKind.Struct, converted.Kind);
|
||||
Assert.Equal(0xA0u, converted.MasterPropertyId);
|
||||
var convertedArray = converted.StructValue[0x55u];
|
||||
Assert.Equal(UiPropertyKind.Array, convertedArray.Kind);
|
||||
Assert.Equal(0xA1u, convertedArray.MasterPropertyId);
|
||||
Assert.Equal(-17, convertedArray.ArrayValue[0].IntegerValue);
|
||||
Assert.Equal(0xFEDCBA9876543210ul, convertedArray.ArrayValue[1].UnsignedValue);
|
||||
Assert.Equal(new Vector3(1.25f, -2.5f, 9.75f), converted.StructValue[0x56u].VectorValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertProperty_PreservesStringInfoAndColorBytes()
|
||||
{
|
||||
var text = new StringInfoBaseProperty
|
||||
{
|
||||
Value = new StringInfo
|
||||
{
|
||||
Token = 7,
|
||||
StringId = 0x12345678u,
|
||||
TableId = 0x23000001u,
|
||||
Override = StringInfoOverrideFlag.AutoGen,
|
||||
English = 2,
|
||||
Comment = 3,
|
||||
},
|
||||
};
|
||||
var color = new ColorBaseProperty
|
||||
{
|
||||
Value = new ColorARGB { Blue = 1, Green = 2, Red = 3, Alpha = 4 },
|
||||
};
|
||||
|
||||
var convertedText = LayoutImporter.ConvertProperty(text);
|
||||
var convertedColor = LayoutImporter.ConvertProperty(color);
|
||||
|
||||
Assert.Equal(
|
||||
new UiStringInfoValue(7, 0x12345678u, 0x23000001u, 2, 2, 3),
|
||||
convertedText.StringInfoValue);
|
||||
Assert.Equal(new UiColorValue(1, 2, 3, 4), convertedColor.ColorValue);
|
||||
}
|
||||
|
||||
private static UiPropertyValue Bool(bool value)
|
||||
=> new() { Kind = UiPropertyKind.Bool, BoolValue = value };
|
||||
|
||||
private static UiPropertyValue Enum(uint value)
|
||||
=> new() { Kind = UiPropertyKind.Enum, UnsignedValue = value };
|
||||
|
||||
private static UiStateInfo State(
|
||||
uint id,
|
||||
string name,
|
||||
params (uint Id, UiPropertyValue Value)[] properties)
|
||||
{
|
||||
var state = new UiStateInfo { Id = id, Name = name };
|
||||
foreach (var property in properties)
|
||||
state.Properties.Values[property.Id] = property.Value;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,10 @@ public class VitalsBindingTests
|
|||
public void Bind_SetsHealthMeterFillFromProvider()
|
||||
{
|
||||
var health = new UiMeter();
|
||||
var layout = FakeLayout((VitalsController.Health, health));
|
||||
var healthText = new UiText();
|
||||
var layout = FakeLayout(
|
||||
(VitalsController.Health, health),
|
||||
(VitalsController.HealthText, healthText));
|
||||
float hp = 0.42f;
|
||||
|
||||
VitalsController.Bind(layout,
|
||||
|
|
@ -28,9 +31,9 @@ public class VitalsBindingTests
|
|||
manaText: () => "");
|
||||
|
||||
Assert.Equal(0.42f, health.Fill()!.Value);
|
||||
// The meter no longer draws its own label; the cur/max is a centered UiText child.
|
||||
// The meter no longer draws its own label; the authored text node is bound in place.
|
||||
Assert.Null(health.Label());
|
||||
Assert.Equal("42/100", NumberText(health));
|
||||
Assert.Equal("42/100", NumberText(healthText));
|
||||
}
|
||||
|
||||
// ── Test 2: All three meters wired to distinct providers ──────────────────
|
||||
|
|
@ -41,10 +44,16 @@ public class VitalsBindingTests
|
|||
var health = new UiMeter();
|
||||
var stamina = new UiMeter();
|
||||
var mana = new UiMeter();
|
||||
var healthText = new UiText();
|
||||
var staminaText = new UiText();
|
||||
var manaText = new UiText();
|
||||
var layout = FakeLayout(
|
||||
(VitalsController.Health, health),
|
||||
(VitalsController.Stamina, stamina),
|
||||
(VitalsController.Mana, mana));
|
||||
(VitalsController.Mana, mana),
|
||||
(VitalsController.HealthText, healthText),
|
||||
(VitalsController.StaminaText, staminaText),
|
||||
(VitalsController.ManaText, manaText));
|
||||
|
||||
VitalsController.Bind(layout,
|
||||
healthPct: () => 0.25f,
|
||||
|
|
@ -56,13 +65,13 @@ public class VitalsBindingTests
|
|||
|
||||
// Each meter should reflect its own provider, not another's.
|
||||
Assert.Equal(0.25f, health.Fill()!.Value);
|
||||
Assert.Equal("25/100", NumberText(health));
|
||||
Assert.Equal("25/100", NumberText(healthText));
|
||||
|
||||
Assert.Equal(0.50f, stamina.Fill()!.Value);
|
||||
Assert.Equal("50/100", NumberText(stamina));
|
||||
Assert.Equal("50/100", NumberText(staminaText));
|
||||
|
||||
Assert.Equal(0.75f, mana.Fill()!.Value);
|
||||
Assert.Equal("75/100", NumberText(mana));
|
||||
Assert.Equal("75/100", NumberText(manaText));
|
||||
}
|
||||
|
||||
// ── Test 3: Missing meter ids are silently skipped (no throw) ─────────────
|
||||
|
|
@ -89,12 +98,12 @@ public class VitalsBindingTests
|
|||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>The cur/max text from the centered <see cref="UiText"/> number that
|
||||
/// <see cref="VitalsController"/> attaches as the meter's child.</summary>
|
||||
private static string NumberText(UiMeter m)
|
||||
/// <summary>The cur/max text from the authored <see cref="UiText"/> node.</summary>
|
||||
private static string NumberText(UiText num)
|
||||
{
|
||||
var num = Assert.IsType<UiText>(m.Children[0]);
|
||||
Assert.True(num.Centered);
|
||||
Assert.True(num.OneLine);
|
||||
Assert.False(num.Selectable);
|
||||
var lines = num.LinesProvider();
|
||||
return lines.Count > 0 ? lines[0].Text : "";
|
||||
}
|
||||
|
|
|
|||
30486
tests/AcDream.App.Tests/UI/Layout/fixtures/character_2100002E.json
Normal file
30486
tests/AcDream.App.Tests/UI/Layout/fixtures/character_2100002E.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
17057
tests/AcDream.App.Tests/UI/Layout/fixtures/inventory_21000023.json
Normal file
17057
tests/AcDream.App.Tests/UI/Layout/fixtures/inventory_21000023.json
Normal file
File diff suppressed because it is too large
Load diff
11218
tests/AcDream.App.Tests/UI/Layout/fixtures/paperdoll_21000024.json
Normal file
11218
tests/AcDream.App.Tests/UI/Layout/fixtures/paperdoll_21000024.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
12227
tests/AcDream.App.Tests/UI/Layout/fixtures/toolbar_21000016.json
Normal file
12227
tests/AcDream.App.Tests/UI/Layout/fixtures/toolbar_21000016.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
39
tests/AcDream.App.Tests/UI/UiButtonStateMachineTests.cs
Normal file
39
tests/AcDream.App.Tests/UI/UiButtonStateMachineTests.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
using AcDream.App.UI;
|
||||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
||||
public sealed class UiButtonStateMachineTests
|
||||
{
|
||||
public static TheoryData<UiButtonVisualInput, uint> RequestedCases
|
||||
=> new()
|
||||
{
|
||||
{ new(false, false, false, false, false), UiButtonStateMachine.Normal },
|
||||
{ new(false, false, true, false, true), UiButtonStateMachine.NormalRollover },
|
||||
{ new(false, false, true, true, false), UiButtonStateMachine.NormalRollover },
|
||||
{ new(false, false, true, true, true), UiButtonStateMachine.NormalPressed },
|
||||
{ new(false, true, false, false, false), UiButtonStateMachine.Highlight },
|
||||
{ new(false, true, true, false, true), UiButtonStateMachine.HighlightRollover },
|
||||
{ new(false, true, true, true, false), UiButtonStateMachine.HighlightRollover },
|
||||
{ new(false, true, true, true, true), UiButtonStateMachine.HighlightPressed },
|
||||
{ new(true, false, true, true, true), UiButtonStateMachine.Ghosted },
|
||||
{ new(true, true, true, true, true), UiButtonStateMachine.Ghosted },
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(RequestedCases))]
|
||||
public void RequestedState_MatchesRetailPrecedence(UiButtonVisualInput input, uint expected)
|
||||
=> Assert.Equal(expected, UiButtonStateMachine.RequestedState(input));
|
||||
|
||||
[Fact]
|
||||
public void ResolveState_MissingRequestedState_PreservesCustomState()
|
||||
{
|
||||
var available = new HashSet<uint> { 0x10000053u };
|
||||
|
||||
uint resolved = UiButtonStateMachine.ResolveState(
|
||||
0x10000053u,
|
||||
new UiButtonVisualInput(false, false, true, false, true),
|
||||
available);
|
||||
|
||||
Assert.Equal(0x10000053u, resolved);
|
||||
}
|
||||
}
|
||||
|
|
@ -22,4 +22,140 @@ public class UiButtonTests
|
|||
var b = new UiButton(new ElementInfo { Type = 1 }, NoTex);
|
||||
Assert.False(b.ClickThrough);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PointerTransitions_UseRetailNormalStates()
|
||||
{
|
||||
var b = ButtonWithStates("Normal", "Normal_rollover", "Normal_pressed");
|
||||
|
||||
b.OnEvent(new UiEvent(0, b, UiEventType.HoverEnter));
|
||||
Assert.Equal("Normal_rollover", b.ActiveState);
|
||||
|
||||
b.OnEvent(new UiEvent(0, b, UiEventType.MouseDown, Data1: 5, Data2: 5));
|
||||
Assert.Equal("Normal_pressed", b.ActiveState);
|
||||
|
||||
b.OnEvent(new UiEvent(0, b, UiEventType.MouseMove, Data1: 50, Data2: 50));
|
||||
Assert.Equal("Normal_rollover", b.ActiveState);
|
||||
|
||||
b.OnEvent(new UiEvent(0, b, UiEventType.MouseMove, Data1: 5, Data2: 5));
|
||||
Assert.Equal("Normal_pressed", b.ActiveState);
|
||||
|
||||
b.OnEvent(new UiEvent(0, b, UiEventType.MouseUp, Data1: 5, Data2: 5));
|
||||
Assert.Equal("Normal_rollover", b.ActiveState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToggleRelease_SelectsHighlightState()
|
||||
{
|
||||
var info = ButtonInfo("Normal", "Highlight");
|
||||
AddBoolProperty(info, 0x0Bu, true);
|
||||
var b = CreateButton(info);
|
||||
|
||||
b.OnEvent(new UiEvent(0, b, UiEventType.MouseDown, Data1: 5, Data2: 5));
|
||||
b.OnEvent(new UiEvent(0, b, UiEventType.MouseUp, Data1: 5, Data2: 5));
|
||||
|
||||
Assert.True(b.Selected);
|
||||
Assert.Equal("Highlight", b.ActiveState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisabledProperty_SelectsGhostedAndSuppressesClick()
|
||||
{
|
||||
var info = ButtonInfo("Normal", "Ghosted");
|
||||
AddBoolProperty(info, 0x0Du, true);
|
||||
var b = CreateButton(info);
|
||||
b.OnClick = () => _clicked = true;
|
||||
|
||||
b.OnEvent(new UiEvent(0, b, UiEventType.Click));
|
||||
|
||||
Assert.False(b.Enabled);
|
||||
Assert.Equal("Ghosted", b.ActiveState);
|
||||
Assert.False(_clicked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MissingStandardState_PreservesCustomSemanticState()
|
||||
{
|
||||
var info = ButtonInfo("LockedUI");
|
||||
info.DefaultStateName = "LockedUI";
|
||||
var b = CreateButton(info);
|
||||
|
||||
b.OnEvent(new UiEvent(0, b, UiEventType.HoverEnter));
|
||||
b.OnEvent(new UiEvent(0, b, UiEventType.MouseDown, Data1: 5, Data2: 5));
|
||||
|
||||
Assert.Equal("LockedUI", b.ActiveState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HotClick_FiresImmediatelyRepeatsAndSuppressesReleaseClick()
|
||||
{
|
||||
var info = ButtonInfo("Normal");
|
||||
AddBoolProperty(info, 0x0Fu, true);
|
||||
AddFloatProperty(info, 0x10u, 0.10f);
|
||||
AddFloatProperty(info, 0x11u, 0.05f);
|
||||
int clicks = 0;
|
||||
var b = CreateButton(info);
|
||||
b.OnClick = () => clicks++;
|
||||
|
||||
b.OnEvent(new UiEvent(0, b, UiEventType.MouseDown, Data1: 5, Data2: 5));
|
||||
Assert.Equal(1, clicks);
|
||||
b.OnGlobalUiTime(1.00);
|
||||
b.OnGlobalUiTime(1.09);
|
||||
Assert.Equal(1, clicks);
|
||||
b.OnGlobalUiTime(1.11);
|
||||
Assert.Equal(2, clicks);
|
||||
|
||||
b.OnEvent(new UiEvent(0, b, UiEventType.MouseUp, Data1: 5, Data2: 5));
|
||||
b.OnEvent(new UiEvent(0, b, UiEventType.Click, Data1: 5, Data2: 5));
|
||||
Assert.Equal(2, clicks);
|
||||
}
|
||||
|
||||
private static UiButton ButtonWithStates(params string[] states)
|
||||
{
|
||||
var info = ButtonInfo(states);
|
||||
AddBoolProperty(info, 0x13u, true);
|
||||
return CreateButton(info);
|
||||
}
|
||||
|
||||
private static ElementInfo ButtonInfo(params string[] states)
|
||||
{
|
||||
var info = new ElementInfo { Type = 1, Width = 20, Height = 20 };
|
||||
uint file = 1;
|
||||
foreach (string state in states)
|
||||
info.StateMedia[state] = (file++, 1);
|
||||
if (states.Length > 0)
|
||||
info.DefaultStateName = states[0];
|
||||
return info;
|
||||
}
|
||||
|
||||
private static void AddBoolProperty(ElementInfo info, uint id, bool value)
|
||||
{
|
||||
if (!info.States.TryGetValue(UiStateInfo.DirectStateId, out var state))
|
||||
{
|
||||
state = new UiStateInfo { Id = UiStateInfo.DirectStateId };
|
||||
info.States[UiStateInfo.DirectStateId] = state;
|
||||
}
|
||||
state.Properties.Values[id] = new UiPropertyValue
|
||||
{
|
||||
Kind = UiPropertyKind.Bool,
|
||||
BoolValue = value,
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddFloatProperty(ElementInfo info, uint id, float value)
|
||||
{
|
||||
if (!info.States.TryGetValue(UiStateInfo.DirectStateId, out var state))
|
||||
{
|
||||
state = new UiStateInfo { Id = UiStateInfo.DirectStateId };
|
||||
info.States[UiStateInfo.DirectStateId] = state;
|
||||
}
|
||||
state.Properties.Values[id] = new UiPropertyValue
|
||||
{
|
||||
Kind = UiPropertyKind.Float,
|
||||
FloatValue = value,
|
||||
};
|
||||
}
|
||||
|
||||
private static UiButton CreateButton(ElementInfo info)
|
||||
=> new(info, NoTex) { Width = info.Width, Height = info.Height };
|
||||
}
|
||||
|
|
|
|||
132
tests/AcDream.App.Tests/UI/UiLayoutPolicyTests.cs
Normal file
132
tests/AcDream.App.Tests/UI/UiLayoutPolicyTests.cs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
using AcDream.App.UI;
|
||||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
||||
public sealed class UiLayoutPolicyTests
|
||||
{
|
||||
private static readonly UiPixelRect OriginalParent =
|
||||
UiPixelRect.FromPositionAndSize(0, 0, 800, 600);
|
||||
private static readonly UiPixelRect OriginalChild =
|
||||
UiPixelRect.FromPositionAndSize(100, 50, 200, 100);
|
||||
|
||||
[Fact]
|
||||
public void ModeZero_PreservesEveryCurrentEdge()
|
||||
{
|
||||
var current = new UiPixelRect(111, 66, 333, 177);
|
||||
var resizedParent = UiPixelRect.FromPositionAndSize(0, 0, 1600, 900);
|
||||
|
||||
var result = UiLayoutPolicy.Apply(
|
||||
0, 0, 0, 0,
|
||||
OriginalChild,
|
||||
OriginalParent,
|
||||
current,
|
||||
resizedParent);
|
||||
|
||||
Assert.Equal(current, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModeOne_KeepsNearEdgesAndAddsParentDeltaToFarEdges()
|
||||
{
|
||||
var resizedParent = UiPixelRect.FromPositionAndSize(0, 0, 1000, 700);
|
||||
|
||||
var result = UiLayoutPolicy.Apply(
|
||||
1, 1, 1, 1,
|
||||
OriginalChild,
|
||||
OriginalParent,
|
||||
OriginalChild,
|
||||
resizedParent);
|
||||
|
||||
Assert.Equal(new UiPixelRect(100, 50, 499, 249), result);
|
||||
Assert.Equal(400, result.Width);
|
||||
Assert.Equal(200, result.Height);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MixedModeTwoAndOne_MovesFixedSizeChildWithFarSide()
|
||||
{
|
||||
var resizedParent = UiPixelRect.FromPositionAndSize(0, 0, 1000, 700);
|
||||
|
||||
var result = UiLayoutPolicy.Apply(
|
||||
2, 2, 1, 1,
|
||||
OriginalChild,
|
||||
OriginalParent,
|
||||
OriginalChild,
|
||||
resizedParent);
|
||||
|
||||
Assert.Equal(new UiPixelRect(300, 150, 499, 249), result);
|
||||
Assert.Equal(OriginalChild.Width, result.Width);
|
||||
Assert.Equal(OriginalChild.Height, result.Height);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModeThree_UsesInclusivePixelCenterFormula()
|
||||
{
|
||||
var resizedParent = UiPixelRect.FromPositionAndSize(0, 0, 1001, 701);
|
||||
|
||||
var result = UiLayoutPolicy.Apply(
|
||||
3, 3, 3, 3,
|
||||
OriginalChild,
|
||||
OriginalParent,
|
||||
OriginalChild,
|
||||
resizedParent);
|
||||
|
||||
Assert.Equal(new UiPixelRect(400, 300, 599, 399), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModeFour_ScalesEachCoordinateWithTruncationTowardZero()
|
||||
{
|
||||
var originalParent = UiPixelRect.FromPositionAndSize(0, 0, 10, 10);
|
||||
var originalChild = new UiPixelRect(-3, -3, -1, -1);
|
||||
var resizedParent = UiPixelRect.FromPositionAndSize(0, 0, 15, 15);
|
||||
|
||||
var result = UiLayoutPolicy.Apply(
|
||||
4, 4, 4, 4,
|
||||
originalChild,
|
||||
originalParent,
|
||||
originalChild,
|
||||
resizedParent);
|
||||
|
||||
Assert.Equal(new UiPixelRect(-4, -4, -1, -1), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AssigningCompatibilityAnchors_OptsOutOfImportedPolicy()
|
||||
{
|
||||
var element = new UiPanel
|
||||
{
|
||||
LayoutPolicy = new UiLayoutPolicy(
|
||||
1, 1, 1, 1,
|
||||
OriginalChild,
|
||||
OriginalParent),
|
||||
};
|
||||
|
||||
element.Anchors = AnchorEdges.Left | AnchorEdges.Top;
|
||||
|
||||
Assert.Null(element.LayoutPolicy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyAnchor_UsesImportedPolicyInsteadOfCompatibilityMargins()
|
||||
{
|
||||
var element = new UiPanel
|
||||
{
|
||||
Left = 100,
|
||||
Top = 50,
|
||||
Width = 200,
|
||||
Height = 100,
|
||||
LayoutPolicy = new UiLayoutPolicy(
|
||||
1, 1, 1, 1,
|
||||
OriginalChild,
|
||||
OriginalParent),
|
||||
};
|
||||
|
||||
element.ApplyAnchor(1000, 700);
|
||||
|
||||
Assert.Equal(100f, element.Left);
|
||||
Assert.Equal(50f, element.Top);
|
||||
Assert.Equal(400f, element.Width);
|
||||
Assert.Equal(200f, element.Height);
|
||||
}
|
||||
}
|
||||
|
|
@ -482,4 +482,99 @@ public class UiRootInputTests
|
|||
Assert.True(clicked.ZOrder > peer.ZOrder);
|
||||
root.OnMouseUp(UiMouseButton.Left, 50, 50);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddChild_AssignsEventIdsRecursively()
|
||||
{
|
||||
var root = new UiRoot { Width = 800, Height = 600 };
|
||||
var parent = new UiPanel { Width = 100, Height = 100 };
|
||||
var child = new UiPanel { Width = 50, Height = 50 };
|
||||
var grandchild = new UiPanel { Width = 25, Height = 25 };
|
||||
child.AddChild(grandchild);
|
||||
parent.AddChild(child);
|
||||
|
||||
root.AddChild(parent);
|
||||
|
||||
Assert.NotEqual(0u, parent.EventId);
|
||||
Assert.NotEqual(0u, child.EventId);
|
||||
Assert.NotEqual(0u, grandchild.EventId);
|
||||
Assert.Equal(3, new[] { parent.EventId, child.EventId, grandchild.EventId }.Distinct().Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemovingSubtree_ClearsAllRootOwnership()
|
||||
{
|
||||
var root = new UiRoot { Width = 800, Height = 600 };
|
||||
var window = new UiPanel { Width = 200, Height = 100 };
|
||||
var field = new UiField { Width = 100, Height = 20 };
|
||||
window.AddChild(field);
|
||||
root.AddChild(window);
|
||||
root.RegisterWindow("test", window);
|
||||
root.DefaultTextInput = field;
|
||||
root.Modal = window;
|
||||
root.SetKeyboardFocus(field);
|
||||
root.SetCapture(field);
|
||||
|
||||
Assert.True(root.RemoveChild(window));
|
||||
|
||||
Assert.Null(root.KeyboardFocus);
|
||||
Assert.Null(root.Captured);
|
||||
Assert.Null(root.DefaultTextInput);
|
||||
Assert.Null(root.Modal);
|
||||
Assert.False(root.ShowWindow("test"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_ChecksTooltipBeforeGlobalTimeMessage_AtRetailDelay()
|
||||
{
|
||||
var root = new UiRoot { Width = 800, Height = 600 };
|
||||
var recorder = new TimeOrderRecorder { Width = 100, Height = 100 };
|
||||
root.AddChild(recorder);
|
||||
|
||||
root.Tick(0, 1_000);
|
||||
root.OnMouseMove(10, 10);
|
||||
recorder.Events.Clear();
|
||||
root.Tick(0, 1_249);
|
||||
Assert.DoesNotContain("tooltip", recorder.Events);
|
||||
recorder.Events.Clear();
|
||||
|
||||
root.Tick(0, 1_250);
|
||||
|
||||
Assert.Equal(new[] { "tooltip", "global" }, recorder.Events);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_GlobalTimeRemoval_SkipsDetachedSiblingWithoutInvalidatingWalk()
|
||||
{
|
||||
var root = new UiRoot { Width = 800, Height = 600 };
|
||||
var victim = new TimeOrderRecorder { Width = 100, Height = 100 };
|
||||
var remover = new GlobalTimeAction(() => root.RemoveChild(victim))
|
||||
{ Width = 100, Height = 100 };
|
||||
root.AddChild(remover);
|
||||
root.AddChild(victim);
|
||||
|
||||
root.Tick(0, 1_000);
|
||||
|
||||
Assert.Empty(victim.Events);
|
||||
Assert.Null(victim.Parent);
|
||||
}
|
||||
|
||||
private sealed class TimeOrderRecorder : UiElement, IUiGlobalTimeListener
|
||||
{
|
||||
public List<string> Events { get; } = new();
|
||||
|
||||
public override bool OnEvent(in UiEvent e)
|
||||
{
|
||||
if (e.Type != UiEventType.Tooltip) return false;
|
||||
Events.Add("tooltip");
|
||||
return true;
|
||||
}
|
||||
|
||||
public void OnGlobalUiTime(double nowSeconds) => Events.Add("global");
|
||||
}
|
||||
|
||||
private sealed class GlobalTimeAction(Action action) : UiElement, IUiGlobalTimeListener
|
||||
{
|
||||
public void OnGlobalUiTime(double nowSeconds) => action();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue