using AcDream.App.Rendering;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.Core.Chat;
using AcDream.UI.Abstractions;
using AcDream.UI.Abstractions.Panels.Chat;
using DatReaderWriter;
using AcDream.Content;
using System.Numerics;
namespace AcDream.App.Studio;
///
/// Builds a multi-window UI desktop for the studio. It mounts several real
/// imported panels into one production so drag, resize,
/// click, focus, and z-order behavior all run through the same UI root as the
/// game.
///
internal static class MockupDesktop
{
public static void Load(IDatReaderWriter dats, RenderStack stack)
{
var objects = SampleData.BuildObjectTable();
var windows = new List();
if (MountVitals(dats, stack, objects) is { } vitals)
RecordWindow(windows, "Vitals", vitals, zOrder: 10);
if (MountToolbar(dats, stack, objects) is { } toolbar)
RecordWindow(windows, "Toolbar", toolbar, zOrder: 20);
if (MountCharacter(dats, stack, objects) is { } character)
RecordWindow(windows, "Character", character, zOrder: 30);
if (MountInventory(dats, stack, objects) is { } inventory)
RecordWindow(windows, "Inventory", inventory, zOrder: 40);
if (MountChat(dats, stack) is { } chat)
RecordWindow(windows, "Chat", chat, zOrder: 50);
MountControls(stack, windows);
}
private static ImportedLayout? Import(IDatReaderWriter dats, uint layoutId, RenderStack stack)
=> LayoutImporter.Import(dats, layoutId, stack.ResolveChrome, stack.VitalsDatFont,
fontResolve: stack.ResolveDatFont);
private static RetailWindowHandle? MountVitals(IDatReaderWriter dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
{
var layout = Import(dats, 0x2100006Cu, stack);
if (layout is null) return null;
IRetainedPanelController? controller =
FixtureProvider.Populate(0x2100006Cu, layout, stack, objects, dats);
return RetailWindowFrame.Mount(
stack.UiHost.Root,
layout.Root,
stack.ResolveChrome,
new RetailWindowFrame.Options
{
WindowName = WindowNames.Vitals,
Chrome = RetailWindowChrome.Imported,
Left = 12f,
Top = 18f,
ResizeX = true,
ResizeY = false,
MinWidth = 40f,
ContentClickThrough = false,
Controller = controller,
});
}
private static RetailWindowHandle? MountToolbar(IDatReaderWriter dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
{
var layout = Import(dats, 0x21000016u, stack);
if (layout is null) return null;
IRetainedPanelController? controller =
FixtureProvider.Populate(0x21000016u, layout, stack, objects, dats);
const int border = RetailChromeSprites.Border;
var toolbarRoot = layout.Root;
float contentW = toolbarRoot.Width > 0f ? toolbarRoot.Width : 300f;
float contentH = toolbarRoot.Height;
RetailWindowHandle handle = RetailWindowFrame.Mount(
stack.UiHost.Root,
toolbarRoot,
stack.ResolveChrome,
new RetailWindowFrame.Options
{
WindowName = WindowNames.Toolbar,
Chrome = RetailWindowChrome.CollapsibleNineSlice,
Left = 12f,
Top = 220f,
ContentWidth = contentW,
ContentHeight = contentH,
Resizable = false,
ResizeX = false,
ResizeY = true,
ResizableEdges = ResizeEdges.Bottom,
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right,
ContentClickThrough = true,
Controller = controller,
});
var frame = (UiCollapsibleFrame)handle.OuterFrame;
ConfigureToolbarCollapse(layout, frame, contentH, border);
return handle;
}
private static void ConfigureToolbarCollapse(ImportedLayout layout, UiCollapsibleFrame frame, float contentH, int border)
{
uint[] row2Ids =
{
0x100006B6u,
0x100006B7u, 0x100006B8u, 0x100006B9u, 0x100006BAu, 0x100006BBu,
0x100006BCu, 0x100006BDu, 0x100006BEu, 0x100006BFu,
0x100006C0u,
};
var row2 = new List();
float minRow2Top = float.MaxValue;
foreach (var id in row2Ids)
{
if (layout.FindElement(id) is not { } element) continue;
row2.Add(element);
if (element.Top < minRow2Top) minRow2Top = element.Top;
}
if (row2.Count == 0) return;
float expandedH = contentH + 2 * border;
float collapsedH = minRow2Top + 2 * border;
frame.CollapsedHeight = collapsedH;
frame.ExpandedHeight = expandedH;
frame.SecondRow = row2;
frame.Resizable = true;
frame.ResizableEdges = ResizeEdges.Bottom;
frame.MinHeight = collapsedH;
frame.MaxHeight = expandedH;
}
private static RetailWindowHandle? MountCharacter(IDatReaderWriter dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
{
var layout = Import(dats, 0x2100002Eu, stack);
if (layout is null) return null;
IRetainedPanelController? controller =
FixtureProvider.Populate(0x2100002Eu, layout, stack, objects, dats);
return RetailWindowFrame.Mount(
stack.UiHost.Root,
layout.Root,
stack.ResolveChrome,
new RetailWindowFrame.Options
{
WindowName = WindowNames.Character,
Chrome = RetailWindowChrome.NineSlice,
Left = 540f,
Top = 18f,
MaxHeight = 760f,
ResizeX = false,
ResizeY = true,
ResizableEdges = ResizeEdges.Bottom,
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom,
ContentClickThrough = false,
Controller = controller,
});
}
private static RetailWindowHandle? MountInventory(IDatReaderWriter dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
{
var layout = Import(dats, 0x21000023u, stack);
if (layout is null) return null;
IRetainedPanelController? controller =
FixtureProvider.Populate(0x21000023u, layout, stack, objects, dats);
var inventoryRoot = layout.Root;
float contentW = inventoryRoot.Width;
float contentH = inventoryRoot.Height;
RetailWindowHandle handle = RetailWindowFrame.Mount(
stack.UiHost.Root,
inventoryRoot,
stack.ResolveChrome,
new RetailWindowFrame.Options
{
WindowName = WindowNames.Inventory,
Chrome = RetailWindowChrome.NineSlice,
Left = 900f,
Top = 18f,
ContentWidth = contentW,
ContentHeight = contentH,
ResizeX = false,
ResizeY = true,
ResizableEdges = ResizeEdges.Bottom,
MaxHeight = Math.Max(
contentH + 2f * RetailChromeSprites.Border,
stack.UiHost.Root.Height - 18f),
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom,
Controller = controller,
});
StretchV(layout, 0x100001D0u);
StretchV(layout, 0x100001CFu);
StretchV(layout, 0x100001C6u);
StretchV(layout, 0x100001C7u);
PinTopLeft(layout, 0x100001CDu);
PinTopLeft(layout, 0x100001CEu);
return handle;
}
private static void StretchV(ImportedLayout layout, uint id)
{
if (layout.FindElement(id) is { } element)
element.Anchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom;
}
private static void PinTopLeft(ImportedLayout layout, uint id)
{
if (layout.FindElement(id) is { } element)
element.Anchors = AnchorEdges.Left | AnchorEdges.Top;
}
private static RetailWindowHandle? MountChat(IDatReaderWriter dats, RenderStack stack)
{
var rootInfo = LayoutImporter.ImportInfos(dats, ChatWindowController.LayoutId);
if (rootInfo is null) return null;
var strings = new DatStringResolver(dats);
var layout = LayoutImporter.Build(rootInfo, stack.ResolveChrome, stack.VitalsDatFont,
fontResolve: stack.ResolveDatFont,
stringResolve: strings.Resolve);
var chatLog = new ChatLog();
chatLog.SetLocalPlayerGuid(SampleData.PlayerGuid);
chatLog.OnSystemMessage("Welcome to the acdream UI mockup.", 0);
chatLog.OnLocalSpeech("You", "This desktop is running through UiHost.", SampleData.PlayerGuid, isRanged: false);
chatLog.OnChannelBroadcast(1, "Caith", "Drag and resize the windows here.", "General");
chatLog.OnTellReceived("Alicia", "The old UI is starting to wake up.", 0x5000ABCDu);
var vm = new ChatVM(chatLog, displayLimit: 200);
var controller = ChatWindowController.Bind(
rootInfo,
layout,
vm,
() => NullCommandBus.Instance,
stack.VitalsDatFont,
debugFont: null,
stack.ResolveChrome);
if (controller is null) return null;
var chatRoot = controller.Root;
RetailWindowHandle handle = RetailWindowFrame.Mount(
stack.UiHost.Root,
chatRoot,
stack.ResolveChrome,
new RetailWindowFrame.Options
{
WindowName = WindowNames.Chat,
Chrome = RetailWindowChrome.NineSlice,
Left = 12f,
Top = 390f,
ContentWidth = 490f,
ContentHeight = chatRoot.Height,
RebaseContentLayout = true,
DatConstraintSource = controller.DatWindowInfo,
MinWidth = 200f,
ResizeX = true,
ResizeY = true,
Opacity = 0.75f,
StateController = controller,
});
controller.AttachWindow(handle);
stack.UiHost.Root.DefaultTextInput = controller.Input;
return handle;
}
private static void RecordWindow(
List windows,
string title,
RetailWindowHandle handle,
int zOrder)
{
UiElement element = handle.OuterFrame;
element.ZOrder = zOrder;
windows.Add(new MockupWindow(
handle.Name,
title,
element,
new MockupRect(element.Left, element.Top, element.Width, element.Height),
zOrder));
}
private static void MountControls(RenderStack stack, IReadOnlyList windows)
{
if (windows.Count == 0) return;
var font = stack.VitalsDatFont;
var panel = new MockupControlPanel
{
Name = "mockup-controls",
Left = 1010f,
Top = 430f,
Width = 258f,
Height = 242f,
Anchors = AnchorEdges.None,
Draggable = true,
Resizable = false,
BackgroundColor = new Vector4(0.03f, 0.035f, 0.04f, 0.82f),
BorderColor = new Vector4(0.55f, 0.45f, 0.27f, 0.95f),
BorderThickness = 1f,
ZOrder = 1_000,
};
panel.AddChild(new MockupLabel(font)
{
Left = 12f,
Top = 10f,
Width = 220f,
Height = 18f,
Text = "Mockup",
TextColor = new Vector4(0.95f, 0.86f, 0.62f, 1f),
});
var toggles = new List<(MockupWindow Window, MockupButton Button)>();
panel.AddChild(MakeButton(font, "Arrange", 12f, 34f, 108f, 24f, () =>
{
Arrange(windows, showAll: false);
RefreshToggles(toggles);
}));
panel.AddChild(MakeButton(font, "Reset", 132f, 34f, 108f, 24f, () =>
{
Arrange(windows, showAll: true);
RefreshToggles(toggles);
}));
float y = 70f;
foreach (var window in windows)
{
var button = MakeButton(font, "", 12f, y, 228f, 24f, () =>
{
stack.UiHost.ToggleWindow(window.Name);
RefreshToggles(toggles);
});
toggles.Add((window, button));
panel.AddChild(button);
y += 30f;
}
RefreshToggles(toggles);
stack.UiHost.Root.AddChild(panel);
}
private static MockupButton MakeButton(UiDatFont? font, string text,
float left, float top, float width, float height, Action onClick)
{
var button = new MockupButton(font)
{
Left = left,
Top = top,
Width = width,
Height = height,
Text = text,
};
button.Click += onClick;
return button;
}
private static void Arrange(IReadOnlyList windows, bool showAll)
{
foreach (var window in windows)
{
var element = window.Element;
element.Left = window.DefaultRect.Left;
element.Top = window.DefaultRect.Top;
element.Width = window.DefaultRect.Width;
element.Height = window.DefaultRect.Height;
element.ZOrder = window.DefaultZOrder;
if (showAll)
element.Visible = true;
}
}
private static void RefreshToggles(IReadOnlyList<(MockupWindow Window, MockupButton Button)> toggles)
{
foreach (var (window, button) in toggles)
{
bool visible = window.Element.Visible;
button.Text = visible ? $"{window.Title} on" : $"{window.Title} off";
button.TextColor = visible
? new Vector4(0.96f, 0.93f, 0.82f, 1f)
: new Vector4(0.62f, 0.62f, 0.62f, 1f);
button.BackgroundColor = visible
? new Vector4(0.14f, 0.13f, 0.10f, 0.94f)
: new Vector4(0.06f, 0.065f, 0.07f, 0.82f);
}
}
private sealed record MockupWindow(
string Name,
string Title,
UiElement Element,
MockupRect DefaultRect,
int DefaultZOrder);
private readonly record struct MockupRect(float Left, float Top, float Width, float Height);
private sealed class MockupControlPanel : UiPanel
{
protected override void OnDraw(UiRenderContext ctx)
{
if (BackgroundColor.W > 0f)
ctx.DrawFill(0f, 0f, Width, Height, BackgroundColor);
if (BorderColor.W > 0f && BorderThickness > 0f)
ctx.DrawRectOutline(0f, 0f, Width, Height, BorderColor, BorderThickness);
}
protected override void OnTick(double deltaSeconds)
{
base.OnTick(deltaSeconds);
if (Parent is null) return;
int top = ZOrder;
foreach (var sibling in Parent.Children)
{
if (!ReferenceEquals(sibling, this))
top = Math.Max(top, sibling.ZOrder + 1);
}
ZOrder = top;
}
}
private sealed class MockupLabel : UiElement
{
private readonly UiDatFont? _font;
public string Text { get; set; } = string.Empty;
public Vector4 TextColor { get; set; } = Vector4.One;
public MockupLabel(UiDatFont? font)
{
_font = font;
ClickThrough = true;
}
protected override void OnDraw(UiRenderContext ctx)
{
if (_font is null || Text.Length == 0) return;
ctx.DrawStringDat(_font, Text, 0f, 0f, TextColor);
}
}
private sealed class MockupButton : UiPanel
{
private readonly UiDatFont? _font;
public event Action? Click;
public string Text { get; set; } = string.Empty;
public Vector4 TextColor { get; set; } = new(0.96f, 0.93f, 0.82f, 1f);
public MockupButton(UiDatFont? font)
{
_font = font;
BackgroundColor = new Vector4(0.14f, 0.13f, 0.10f, 0.94f);
BorderColor = new Vector4(0.45f, 0.38f, 0.24f, 1f);
BorderThickness = 1f;
}
public override bool HandlesClick => true;
public override bool OnEvent(in UiEvent e)
{
if (e.Type != UiEventType.Click || !Enabled) return false;
Click?.Invoke();
return true;
}
protected override void OnDraw(UiRenderContext ctx)
{
if (BackgroundColor.W > 0f)
ctx.DrawFill(0f, 0f, Width, Height, BackgroundColor);
if (BorderColor.W > 0f && BorderThickness > 0f)
ctx.DrawRectOutline(0f, 0f, Width, Height, BorderColor, BorderThickness);
if (_font is null || Text.Length == 0) return;
float textW = _font.MeasureWidth(Text);
float tx = MathF.Max(4f, (Width - textW) * 0.5f);
float ty = (Height - _font.LineHeight) * 0.5f;
ctx.DrawStringDat(_font, Text, tx, ty, TextColor);
}
}
}