feat(ui): centralize retained window lifecycle
This commit is contained in:
parent
4bb37e302e
commit
6e9e10367f
12 changed files with 778 additions and 57 deletions
289
src/AcDream.App/UI/RetailWindowManager.cs
Normal file
289
src/AcDream.App/UI/RetailWindowManager.cs
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Owns the named retained-window registry, raise policy, lifecycle delivery,
|
||||
/// and controller teardown. <see cref="UiRoot"/> remains responsible for raw
|
||||
/// input ownership and reports completed interactions to this manager.
|
||||
/// </summary>
|
||||
public sealed class RetailWindowManager : IDisposable
|
||||
{
|
||||
private readonly UiRoot _root;
|
||||
private readonly Dictionary<string, RetailWindowHandle> _byName =
|
||||
new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<UiElement, RetailWindowHandle> _byFrame = new();
|
||||
private readonly Dictionary<RetailWindowHandle, UiElement> _defaultInputs = new();
|
||||
private bool _disposed;
|
||||
|
||||
internal RetailWindowManager(UiRoot root)
|
||||
{
|
||||
_root = root;
|
||||
_root.ElementVisibilityChanged += OnElementVisibilityChanged;
|
||||
_root.KeyboardFocusChanged += OnKeyboardFocusChanged;
|
||||
_root.PointerCaptureChanged += OnPointerCaptureChanged;
|
||||
_root.WindowMoved += OnWindowMoved;
|
||||
_root.WindowResized += OnWindowResized;
|
||||
_root.UiLockChanged += OnUiLockChanged;
|
||||
}
|
||||
|
||||
public bool IsLocked => _root.UiLocked;
|
||||
public IReadOnlyCollection<RetailWindowHandle> Windows => _byName.Values;
|
||||
|
||||
public RetailWindowHandle Register(
|
||||
string name,
|
||||
UiElement outerFrame,
|
||||
UiElement? contentRoot = null,
|
||||
IRetainedPanelController? controller = null)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||
ArgumentNullException.ThrowIfNull(outerFrame);
|
||||
|
||||
if (!ReferenceEquals(outerFrame.Parent, _root))
|
||||
throw new InvalidOperationException(
|
||||
$"Retained window '{name}' must be mounted as a direct UiRoot child before registration.");
|
||||
|
||||
if (_byName.TryGetValue(name, out var sameName))
|
||||
{
|
||||
if (ReferenceEquals(sameName.OuterFrame, outerFrame)
|
||||
&& ReferenceEquals(sameName.ContentRoot, contentRoot ?? outerFrame)
|
||||
&& ReferenceEquals(sameName.Controller, controller))
|
||||
return sameName;
|
||||
|
||||
Unregister(name);
|
||||
}
|
||||
|
||||
if (_byFrame.TryGetValue(outerFrame, out var sameFrame))
|
||||
Unregister(sameFrame.Name);
|
||||
|
||||
var handle = new RetailWindowHandle(
|
||||
this,
|
||||
name,
|
||||
outerFrame,
|
||||
contentRoot ?? outerFrame,
|
||||
controller);
|
||||
_byName.Add(name, handle);
|
||||
_byFrame.Add(outerFrame, handle);
|
||||
handle.NotifyInitialState();
|
||||
return handle;
|
||||
}
|
||||
|
||||
public bool TryGet(string name, out RetailWindowHandle handle)
|
||||
=> _byName.TryGetValue(name, out handle!);
|
||||
|
||||
internal bool TryGet(UiElement outerFrame, out RetailWindowHandle handle)
|
||||
=> _byFrame.TryGetValue(outerFrame, out handle!);
|
||||
|
||||
public bool Show(string name)
|
||||
{
|
||||
if (!_byName.TryGetValue(name, out var handle)) return false;
|
||||
handle.OuterFrame.Visible = true;
|
||||
BringToFront(handle.OuterFrame);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Hide(string name)
|
||||
{
|
||||
if (!_byName.TryGetValue(name, out var handle)) return false;
|
||||
handle.OuterFrame.Visible = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Toggle(string name)
|
||||
{
|
||||
if (!_byName.TryGetValue(name, out var handle)) return false;
|
||||
if (handle.OuterFrame.Visible)
|
||||
{
|
||||
Hide(name);
|
||||
return false;
|
||||
}
|
||||
|
||||
Show(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Close(string name)
|
||||
{
|
||||
if (!_byName.TryGetValue(name, out var handle)) return false;
|
||||
Hide(name);
|
||||
handle.NotifyClosed();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsVisible(string name)
|
||||
=> _byName.TryGetValue(name, out var handle) && handle.OuterFrame.Visible;
|
||||
|
||||
public bool MoveTo(string name, float left, float top)
|
||||
{
|
||||
if (!_byName.TryGetValue(name, out var handle)) return false;
|
||||
var frame = handle.OuterFrame;
|
||||
if (frame.ConstrainDragToParent && frame.Parent is { } parent)
|
||||
{
|
||||
left = Math.Clamp(left, 0f, Math.Max(0f, parent.Width - frame.Width));
|
||||
top = Math.Clamp(top, 0f, Math.Max(0f, parent.Height - frame.Height));
|
||||
}
|
||||
if (frame.Left == left && frame.Top == top) return true;
|
||||
frame.Left = left;
|
||||
frame.Top = top;
|
||||
_root.NotifyWindowMoved(frame);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ResizeTo(string name, float width, float height)
|
||||
{
|
||||
if (!_byName.TryGetValue(name, out var handle)) return false;
|
||||
var frame = handle.OuterFrame;
|
||||
if (!frame.ResizeX) width = frame.Width;
|
||||
if (!frame.ResizeY) height = frame.Height;
|
||||
width = Math.Clamp(width, frame.MinWidth, float.MaxValue);
|
||||
height = Math.Clamp(height, frame.MinHeight, frame.MaxHeight);
|
||||
if (frame.Width == width && frame.Height == height) return true;
|
||||
frame.Width = width;
|
||||
frame.Height = height;
|
||||
_root.NotifyWindowResized(frame);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SetOpacity(string name, float opacity)
|
||||
{
|
||||
if (!_byName.TryGetValue(name, out var handle)) return false;
|
||||
handle.OuterFrame.Opacity = Math.Clamp(opacity, 0f, 1f);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetLocked(bool locked) => _root.UiLocked = locked;
|
||||
|
||||
public bool Unregister(string name)
|
||||
{
|
||||
if (!_byName.TryGetValue(name, out var handle)) return false;
|
||||
|
||||
if (handle.OuterFrame.Visible)
|
||||
handle.OuterFrame.Visible = false;
|
||||
else
|
||||
_root.ClearSubtreeOwnership(handle.OuterFrame);
|
||||
|
||||
_byName.Remove(name);
|
||||
_byFrame.Remove(handle.OuterFrame);
|
||||
_defaultInputs.Remove(handle);
|
||||
handle.NotifyClosed();
|
||||
handle.DisposeController();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void BringToFront(UiElement window)
|
||||
{
|
||||
int top = window.ZOrder;
|
||||
foreach (var child in _root.Children)
|
||||
if (!ReferenceEquals(child, window))
|
||||
top = Math.Max(top, child.ZOrder + 1);
|
||||
window.ZOrder = top;
|
||||
}
|
||||
|
||||
internal void PrepareToHide(UiElement subtree)
|
||||
{
|
||||
if (!_byFrame.TryGetValue(subtree, out var handle)) return;
|
||||
if (_root.DefaultTextInput is { } input && IsWithin(input, subtree))
|
||||
_defaultInputs[handle] = input;
|
||||
}
|
||||
|
||||
internal void OnSubtreeRemoving(UiElement subtree)
|
||||
{
|
||||
foreach (var handle in _byName.Values
|
||||
.Where(handle => IsWithin(handle.OuterFrame, subtree))
|
||||
.ToArray())
|
||||
{
|
||||
handle.NotifyVisibility(false);
|
||||
_byName.Remove(handle.Name);
|
||||
_byFrame.Remove(handle.OuterFrame);
|
||||
_defaultInputs.Remove(handle);
|
||||
handle.NotifyClosed();
|
||||
handle.DisposeController();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnElementVisibilityChanged(UiElement element, bool visible)
|
||||
{
|
||||
if (!_byFrame.TryGetValue(element, out var handle)) return;
|
||||
|
||||
if (visible
|
||||
&& _root.DefaultTextInput is null
|
||||
&& _defaultInputs.TryGetValue(handle, out var input)
|
||||
&& IsWithin(input, handle.OuterFrame))
|
||||
{
|
||||
_root.DefaultTextInput = input;
|
||||
}
|
||||
|
||||
handle.NotifyVisibility(visible);
|
||||
}
|
||||
|
||||
private void OnKeyboardFocusChanged(UiElement? oldFocus, UiElement? newFocus)
|
||||
{
|
||||
foreach (var handle in _byName.Values.ToArray())
|
||||
{
|
||||
bool hadFocus = IsWithin(oldFocus, handle.OuterFrame);
|
||||
bool hasFocus = IsWithin(newFocus, handle.OuterFrame);
|
||||
if (hadFocus || hasFocus)
|
||||
handle.NotifyDescendantFocusChanged(hasFocus ? newFocus : null);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPointerCaptureChanged(UiElement? oldCapture, UiElement? newCapture)
|
||||
{
|
||||
foreach (var handle in _byName.Values.ToArray())
|
||||
{
|
||||
bool hadCapture = IsWithin(oldCapture, handle.OuterFrame);
|
||||
bool hasCapture = IsWithin(newCapture, handle.OuterFrame);
|
||||
if (hadCapture || hasCapture)
|
||||
handle.NotifyDescendantCaptureChanged(hasCapture ? newCapture : null);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWindowMoved(string name, UiElement window)
|
||||
{
|
||||
if (_byName.TryGetValue(name, out var handle)
|
||||
&& ReferenceEquals(handle.OuterFrame, window))
|
||||
handle.NotifyMoved();
|
||||
}
|
||||
|
||||
private void OnWindowResized(string name, UiElement window)
|
||||
{
|
||||
if (_byName.TryGetValue(name, out var handle)
|
||||
&& ReferenceEquals(handle.OuterFrame, window))
|
||||
handle.NotifyResized();
|
||||
}
|
||||
|
||||
private void OnUiLockChanged(bool locked)
|
||||
{
|
||||
foreach (var handle in _byName.Values.ToArray())
|
||||
handle.NotifyLockChanged(locked);
|
||||
}
|
||||
|
||||
private static bool IsWithin(UiElement? element, UiElement subtree)
|
||||
{
|
||||
while (element is not null)
|
||||
{
|
||||
if (ReferenceEquals(element, subtree)) return true;
|
||||
element = element.Parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
foreach (string name in _byName.Keys.ToArray())
|
||||
Unregister(name);
|
||||
|
||||
_root.ElementVisibilityChanged -= OnElementVisibilityChanged;
|
||||
_root.KeyboardFocusChanged -= OnKeyboardFocusChanged;
|
||||
_root.PointerCaptureChanged -= OnPointerCaptureChanged;
|
||||
_root.WindowMoved -= OnWindowMoved;
|
||||
_root.WindowResized -= OnWindowResized;
|
||||
_root.UiLockChanged -= OnUiLockChanged;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue