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