feat(studio): add mockup desktop controls

This commit is contained in:
Erik 2026-06-26 13:10:34 +02:00
parent 9444a328fa
commit 4cc565badc
3 changed files with 255 additions and 23 deletions

View file

@ -5,6 +5,7 @@ using AcDream.Core.Chat;
using AcDream.UI.Abstractions;
using AcDream.UI.Abstractions.Panels.Chat;
using DatReaderWriter;
using System.Numerics;
namespace AcDream.App.Studio;
@ -19,24 +20,32 @@ internal static class MockupDesktop
public static void Load(DatCollection dats, RenderStack stack)
{
var objects = SampleData.BuildObjectTable();
var windows = new List<MockupWindow>();
MountVitals(dats, stack);
MountToolbar(dats, stack, objects);
MountCharacter(dats, stack, objects);
MountInventory(dats, stack, objects);
MountChat(dats, stack);
if (MountVitals(dats, stack, objects) is { } vitals)
RegisterWindow(stack, windows, WindowNames.Vitals, "Vitals", vitals, zOrder: 10);
if (MountToolbar(dats, stack, objects) is { } toolbar)
RegisterWindow(stack, windows, WindowNames.Toolbar, "Toolbar", toolbar, zOrder: 20);
if (MountCharacter(dats, stack, objects) is { } character)
RegisterWindow(stack, windows, WindowNames.Character, "Character", character, zOrder: 30);
if (MountInventory(dats, stack, objects) is { } inventory)
RegisterWindow(stack, windows, WindowNames.Inventory, "Inventory", inventory, zOrder: 40);
if (MountChat(dats, stack) is { } chat)
RegisterWindow(stack, windows, WindowNames.Chat, "Chat", chat, zOrder: 50);
MountControls(stack, windows);
}
private static ImportedLayout? Import(DatCollection dats, uint layoutId, RenderStack stack)
=> LayoutImporter.Import(dats, layoutId, stack.ResolveChrome, stack.VitalsDatFont,
fontResolve: stack.ResolveDatFont);
private static void MountVitals(DatCollection dats, RenderStack stack)
private static UiElement? MountVitals(DatCollection dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
{
var layout = Import(dats, 0x2100006Cu, stack);
if (layout is null) return;
if (layout is null) return null;
FixtureProvider.Populate(0x2100006Cu, layout, stack, SampleData.BuildObjectTable(), dats);
FixtureProvider.Populate(0x2100006Cu, layout, stack, objects, dats);
var root = layout.Root;
root.Left = 12f;
@ -48,13 +57,13 @@ internal static class MockupDesktop
root.ResizeX = true;
root.ResizeY = false;
root.MinWidth = 40f;
stack.UiHost.Root.AddChild(root);
return root;
}
private static void MountToolbar(DatCollection dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
private static UiElement? MountToolbar(DatCollection dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
{
var layout = Import(dats, 0x21000016u, stack);
if (layout is null) return;
if (layout is null) return null;
FixtureProvider.Populate(0x21000016u, layout, stack, objects, dats);
@ -82,7 +91,7 @@ internal static class MockupDesktop
frame.AddChild(toolbarRoot);
ConfigureToolbarCollapse(layout, frame, contentH, border);
stack.UiHost.Root.AddChild(frame);
return frame;
}
private static void ConfigureToolbarCollapse(ImportedLayout layout, UiCollapsibleFrame frame, float contentH, int border)
@ -117,10 +126,10 @@ internal static class MockupDesktop
frame.MaxHeight = expandedH;
}
private static void MountCharacter(DatCollection dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
private static UiElement? MountCharacter(DatCollection dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
{
var layout = Import(dats, 0x2100002Eu, stack);
if (layout is null) return;
if (layout is null) return null;
FixtureProvider.Populate(0x2100002Eu, layout, stack, objects, dats);
@ -133,13 +142,13 @@ internal static class MockupDesktop
root.Resizable = true;
root.MinWidth = root.Width;
root.MinHeight = root.Height;
stack.UiHost.Root.AddChild(root);
return root;
}
private static void MountInventory(DatCollection dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
private static UiElement? MountInventory(DatCollection dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
{
var layout = Import(dats, 0x21000023u, stack);
if (layout is null) return;
if (layout is null) return null;
FixtureProvider.Populate(0x21000023u, layout, stack, objects, dats);
@ -179,8 +188,7 @@ internal static class MockupDesktop
PinTopLeft(layout, 0x100001CDu);
PinTopLeft(layout, 0x100001CEu);
stack.UiHost.Root.AddChild(frame);
stack.UiHost.RegisterWindow(WindowNames.Inventory, frame);
return frame;
}
private static void StretchV(ImportedLayout layout, uint id)
@ -195,10 +203,10 @@ internal static class MockupDesktop
element.Anchors = AnchorEdges.Left | AnchorEdges.Top;
}
private static void MountChat(DatCollection dats, RenderStack stack)
private static UiElement? MountChat(DatCollection dats, RenderStack stack)
{
var rootInfo = LayoutImporter.ImportInfos(dats, ChatWindowController.LayoutId);
if (rootInfo is null) return;
if (rootInfo is null) return null;
var layout = LayoutImporter.Build(rootInfo, stack.ResolveChrome, stack.VitalsDatFont,
fontResolve: stack.ResolveDatFont);
@ -219,7 +227,7 @@ internal static class MockupDesktop
stack.VitalsDatFont,
debugFont: null,
stack.ResolveChrome);
if (controller is null) return;
if (controller is null) return null;
const int border = RetailChromeSprites.Border;
var chatRoot = controller.Root;
@ -245,7 +253,221 @@ internal static class MockupDesktop
chatRoot.Resizable = false;
frame.AddChild(chatRoot);
stack.UiHost.Root.AddChild(frame);
stack.UiHost.Root.DefaultTextInput = controller.Input;
return frame;
}
private static void RegisterWindow(RenderStack stack, List<MockupWindow> windows,
string name, string title, UiElement element, int zOrder)
{
element.ZOrder = zOrder;
stack.UiHost.Root.AddChild(element);
stack.UiHost.RegisterWindow(name, element);
windows.Add(new MockupWindow(
name,
title,
element,
new MockupRect(element.Left, element.Top, element.Width, element.Height),
zOrder));
}
private static void MountControls(RenderStack stack, IReadOnlyList<MockupWindow> 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<MockupWindow> 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);
}
}
}

View file

@ -108,6 +108,12 @@ public sealed class UiHost : System.IDisposable
/// <summary>Register a top-level window for Show/Hide/Toggle. See <see cref="UiRoot.RegisterWindow"/>.</summary>
public void RegisterWindow(string name, UiElement window) => Root.RegisterWindow(name, window);
/// <summary>Show a registered window; returns false if the name is unknown.</summary>
public bool ShowWindow(string name) => Root.ShowWindow(name);
/// <summary>Hide a registered window; returns false if the name is unknown.</summary>
public bool HideWindow(string name) => Root.HideWindow(name);
/// <summary>Toggle a registered window's visibility; returns the new IsVisible.</summary>
public bool ToggleWindow(string name) => Root.ToggleWindow(name);

View file

@ -4,5 +4,9 @@ namespace AcDream.App.UI;
/// mount, the window registry, and the toggle keybind all agree on one literal.</summary>
public static class WindowNames
{
public const string Vitals = "vitals";
public const string Toolbar = "toolbar";
public const string Character = "character";
public const string Inventory = "inventory";
public const string Chat = "chat";
}