feat(ui): centralize retained window lifecycle

This commit is contained in:
Erik 2026-07-10 21:31:18 +02:00
parent 4bb37e302e
commit 6e9e10367f
12 changed files with 778 additions and 57 deletions

View file

@ -0,0 +1,195 @@
using System.Collections.Generic;
using AcDream.App.UI;
namespace AcDream.App.Tests.UI;
public sealed class RetailWindowManagerTests
{
[Fact]
public void Hide_ClearsRootOwnership_AndShowRestoresDefaultInput()
{
var root = new UiRoot { Width = 800, Height = 600 };
var frame = new UiPanel { Width = 300, Height = 150 };
var input = new UiField { Width = 200, Height = 20 };
frame.AddChild(input);
root.AddChild(frame);
root.DefaultTextInput = input;
root.Modal = frame;
root.SetKeyboardFocus(input);
root.SetCapture(input);
var controller = new RecordingController();
RetailWindowHandle handle = root.RegisterWindow("chat", frame, frame, controller);
var events = new List<string>();
handle.Hidden += _ => events.Add("hidden");
handle.Shown += _ => events.Add("shown");
frame.Visible = false;
Assert.False(frame.Visible);
Assert.Null(root.KeyboardFocus);
Assert.Null(root.Captured);
Assert.Null(root.Modal);
Assert.Null(root.DefaultTextInput);
Assert.Equal(1, controller.ShownCount);
Assert.Equal(1, controller.HiddenCount);
Assert.Equal(new[] { "hidden" }, events);
Assert.True(handle.Show());
Assert.True(frame.Visible);
Assert.Same(input, root.DefaultTextInput);
Assert.Equal(2, controller.ShownCount);
Assert.Equal(new[] { "hidden", "shown" }, events);
}
[Fact]
public void Handle_ReportsGeometryCloseAndLockLifecycle()
{
var root = new UiRoot { Width = 800, Height = 600 };
var frame = new UiPanel
{
Left = 10,
Top = 20,
Width = 100,
Height = 80,
MinWidth = 50,
MinHeight = 40,
};
root.AddChild(frame);
RetailWindowHandle handle = root.RegisterWindow("inventory", frame);
int moved = 0, resized = 0, hidden = 0, shown = 0, closed = 0;
var locks = new List<bool>();
handle.Moved += _ => moved++;
handle.Resized += _ => resized++;
handle.Hidden += _ => hidden++;
handle.Shown += _ => shown++;
handle.Closed += _ => closed++;
handle.LockChanged += (_, locked) => locks.Add(locked);
Assert.True(handle.MoveTo(30, 45));
Assert.Equal((30f, 45f), (frame.Left, frame.Top));
Assert.Equal(1, moved);
Assert.True(handle.ResizeTo(160, 120));
Assert.Equal((160f, 120f), (frame.Width, frame.Height));
Assert.Equal(1, resized);
root.WindowManager.SetLocked(true);
root.WindowManager.SetLocked(true);
root.WindowManager.SetLocked(false);
Assert.Equal(new[] { true, false }, locks);
Assert.True(handle.Close());
Assert.True(handle.Close());
Assert.False(handle.IsVisible);
Assert.Equal(1, hidden);
Assert.Equal(1, closed);
Assert.True(handle.Show());
Assert.True(handle.Close());
Assert.Equal(1, shown);
Assert.Equal(2, hidden);
Assert.Equal(2, closed);
}
[Fact]
public void PointerResize_ReportsCompletionThroughTypedHandle()
{
var root = new UiRoot { Width = 800, Height = 600 };
var frame = new UiPanel
{
Left = 10,
Top = 20,
Width = 100,
Height = 80,
Resizable = true,
};
root.AddChild(frame);
RetailWindowHandle handle = root.RegisterWindow("chat", frame);
int resized = 0;
handle.Resized += _ => resized++;
root.OnMouseDown(UiMouseButton.Left, 109, 60);
root.OnMouseMove(149, 60);
root.OnMouseUp(UiMouseButton.Left, 149, 60);
Assert.Equal(140f, frame.Width);
Assert.Equal(1, resized);
}
[Fact]
public void FocusAndCaptureTransitions_AreScopedToOwningHandle()
{
var root = new UiRoot { Width = 800, Height = 600 };
var first = new UiPanel { Width = 200, Height = 100 };
var second = new UiPanel { Left = 250, Width = 200, Height = 100 };
var firstField = new UiField { Width = 100, Height = 20 };
var secondField = new UiField { Width = 100, Height = 20 };
first.AddChild(firstField);
second.AddChild(secondField);
root.AddChild(first);
root.AddChild(second);
var firstController = new RecordingController();
var secondController = new RecordingController();
RetailWindowHandle firstHandle = root.RegisterWindow("first", first, first, firstController);
RetailWindowHandle secondHandle = root.RegisterWindow("second", second, second, secondController);
var firstCaptures = new List<UiElement?>();
var secondCaptures = new List<UiElement?>();
firstHandle.DescendantCaptureChanged += (_, element) => firstCaptures.Add(element);
secondHandle.DescendantCaptureChanged += (_, element) => secondCaptures.Add(element);
root.SetKeyboardFocus(firstField);
root.SetKeyboardFocus(secondField);
root.SetCapture(firstField);
root.SetCapture(secondField);
root.ReleaseCapture();
Assert.Equal(new UiElement?[] { firstField, null }, firstController.FocusChanges);
Assert.Equal(new UiElement?[] { secondField }, secondController.FocusChanges);
Assert.Equal(new UiElement?[] { firstField, null }, firstCaptures);
Assert.Equal(new UiElement?[] { secondField, null }, secondCaptures);
}
[Fact]
public void UnregisterAndTreeRemoval_DisposeControllersExactlyOnce()
{
var root = new UiRoot { Width = 800, Height = 600 };
var first = new UiPanel { Width = 100, Height = 100 };
var second = new UiPanel { Width = 100, Height = 100 };
root.AddChild(first);
root.AddChild(second);
var firstController = new RecordingController();
var secondController = new RecordingController();
RetailWindowHandle firstHandle = root.RegisterWindow("first", first, first, firstController);
RetailWindowHandle secondHandle = root.RegisterWindow("second", second, second, secondController);
Assert.True(root.UnregisterWindow("first"));
Assert.False(firstHandle.IsRegistered);
Assert.Equal(1, firstController.DisposeCount);
Assert.False(root.ShowWindow("first"));
Assert.True(root.RemoveChild(second));
Assert.False(secondHandle.IsRegistered);
Assert.Equal(1, secondController.DisposeCount);
Assert.False(root.ShowWindow("second"));
root.WindowManager.Dispose();
Assert.Equal(1, firstController.DisposeCount);
Assert.Equal(1, secondController.DisposeCount);
}
private sealed class RecordingController : IRetainedPanelController
{
public int ShownCount { get; private set; }
public int HiddenCount { get; private set; }
public int DisposeCount { get; private set; }
public List<UiElement?> FocusChanges { get; } = new();
public void OnShown() => ShownCount++;
public void OnHidden() => HiddenCount++;
public void OnDescendantFocusChanged(UiElement? focusedDescendant)
=> FocusChanges.Add(focusedDescendant);
public void Dispose() => DisposeCount++;
}
}