feat(ui): port retained widget foundations

This commit is contained in:
Erik 2026-07-10 17:55:41 +02:00
parent 44f9ec13d9
commit d825572e31
44 changed files with 84813 additions and 292 deletions

View file

@ -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();
}
}