using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; using Silk.NET.Input; using Silk.NET.OpenGL; namespace AcDream.App.UI; /// /// Packages the , the 2D sprite batcher /// (), and a default font so /// GameWindow can wire the retail-style UI in with one /// construction and a handful of input callbacks. /// /// Usage (from GameWindow.OnLoad): /// /// _uiHost = new UiHost(_gl, shadersDir, _debugFont); /// _uiHost.Root.WorldMouseFallThrough += (btn, x, y, f) => HandleWorldClick(btn, x, y); /// _uiHost.Root.WorldKeyFallThrough += (vk, lp) => HandleHotkey(vk); /// /// foreach (var mouse in _input.Mice) /// _uiHost.WireMouse(mouse); /// foreach (var kb in _input.Keyboards) /// _uiHost.WireKeyboard(kb); /// /// /// And per frame (from GameWindow.OnRender): /// /// _uiHost.Tick(deltaSeconds); /// _uiHost.Draw(new Vector2(_window!.Size.X, _window.Size.Y)); /// /// /// Retail analog: the trio of DAT_00870340 (Core, owns fonts/atlases), /// DAT_00837ff4 (Device, owns input state), DAT_00870c2c /// (Keystone root, widget tree). We fuse them into a single host class /// because we're not linking to Keystone. /// public sealed class UiHost : System.IDisposable { public UiRoot Root { get; } = new(); public RetailWindowManager WindowManager => Root.WindowManager; public TextRenderer TextRenderer { get; } public BitmapFont? DefaultFont { get; set; } /// The last wired keyboard. Exposed so widgets that need clipboard /// access () or modifier-key state /// () — e.g. 's /// Ctrl+C copy — can reach the device. One-keyboard desktop: last wins. public IKeyboard? Keyboard { get; private set; } private long _startTicks = System.Environment.TickCount64; private readonly List _inputUnsubscribers = new(); private bool _disposed; public UiHost(GL gl, string shaderDir, BitmapFont? defaultFont = null) { TextRenderer = new TextRenderer(gl, shaderDir); DefaultFont = defaultFont; } // ── Per-frame ────────────────────────────────────────────────────── public void Tick(double deltaSeconds) { long now = System.Environment.TickCount64 - _startTicks; Root.Tick(deltaSeconds, now); } public void Draw(Vector2 screenSize) { // Set UiRoot bounds to full screen so HitTestTopDown works. Root.Width = screenSize.X; Root.Height = screenSize.Y; var ctx = new UiRenderContext(TextRenderer, screenSize, DefaultFont); TextRenderer.Begin(screenSize); Root.Draw(ctx); TextRenderer.Flush(DefaultFont); } // ── Input wiring helpers ─────────────────────────────────────────── public void WireMouse(IMouse mouse) { System.ObjectDisposedException.ThrowIf(_disposed, this); System.ArgumentNullException.ThrowIfNull(mouse); void OnMouseDown(IMouse sender, MouseButton button) => Root.OnMouseDown(MapButton(button), (int)sender.Position.X, (int)sender.Position.Y); void OnMouseUp(IMouse sender, MouseButton button) => Root.OnMouseUp(MapButton(button), (int)sender.Position.X, (int)sender.Position.Y); void OnMouseMove(IMouse sender, Vector2 position) => Root.OnMouseMove((int)position.X, (int)position.Y); void OnScroll(IMouse sender, ScrollWheel scroll) => Root.OnScroll((int)scroll.Y); mouse.MouseDown += OnMouseDown; mouse.MouseUp += OnMouseUp; mouse.MouseMove += OnMouseMove; mouse.Scroll += OnScroll; _inputUnsubscribers.Add(() => { mouse.MouseDown -= OnMouseDown; mouse.MouseUp -= OnMouseUp; mouse.MouseMove -= OnMouseMove; mouse.Scroll -= OnScroll; }); } public void WireKeyboard(IKeyboard kb) { System.ObjectDisposedException.ThrowIf(_disposed, this); System.ArgumentNullException.ThrowIfNull(kb); Keyboard = kb; // last wired keyboard wins (one-keyboard desktop) void OnKeyDown(IKeyboard sender, Key key, int scanCode) => Root.OnKeyDown((int)key); void OnKeyUp(IKeyboard sender, Key key, int scanCode) => Root.OnKeyUp((int)key); void OnKeyChar(IKeyboard sender, char value) => Root.OnChar(value); kb.KeyDown += OnKeyDown; kb.KeyUp += OnKeyUp; kb.KeyChar += OnKeyChar; _inputUnsubscribers.Add(() => { kb.KeyDown -= OnKeyDown; kb.KeyUp -= OnKeyUp; kb.KeyChar -= OnKeyChar; }); } private static UiMouseButton MapButton(MouseButton b) => b switch { MouseButton.Left => UiMouseButton.Left, MouseButton.Right => UiMouseButton.Right, MouseButton.Middle => UiMouseButton.Middle, _ => UiMouseButton.Left, }; // ── Window manager forwarders (delegate to UiRoot) ───────────────── /// Register a top-level window for Show/Hide/Toggle. See . public RetailWindowHandle RegisterWindow( string name, UiElement window, UiElement? contentRoot = null, IRetainedPanelController? controller = null, IRetainedWindowStateController? stateController = null) => Root.RegisterWindow(name, window, contentRoot, controller, stateController); public bool UnregisterWindow(string name) => Root.UnregisterWindow(name); /// Show a registered window; returns false if the name is unknown. public bool ShowWindow(string name) => Root.ShowWindow(name); /// Hide a registered window; returns false if the name is unknown. public bool HideWindow(string name) => Root.HideWindow(name); public bool CloseWindow(string name) => Root.CloseWindow(name); /// Return the current visibility of a registered window. public bool IsWindowVisible(string name) => Root.IsWindowVisible(name); /// Toggle a registered window's visibility; returns the new IsVisible. public bool ToggleWindow(string name) => Root.ToggleWindow(name); public void Dispose() { if (_disposed) return; _disposed = true; for (int i = _inputUnsubscribers.Count - 1; i >= 0; i--) _inputUnsubscribers[i](); _inputUnsubscribers.Clear(); Keyboard = null; WindowManager.Dispose(); TextRenderer.Dispose(); } }