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 TextRenderer TextRenderer { get; } public BitmapFont? DefaultFont { get; set; } private long _startTicks = System.Environment.TickCount64; 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) { mouse.MouseDown += (_, b) => Root.OnMouseDown(MapButton(b), (int)mouse.Position.X, (int)mouse.Position.Y); mouse.MouseUp += (_, b) => Root.OnMouseUp(MapButton(b), (int)mouse.Position.X, (int)mouse.Position.Y); mouse.MouseMove += (_, p) => Root.OnMouseMove((int)p.X, (int)p.Y); mouse.Scroll += (_, s) => Root.OnScroll((int)s.Y); } public void WireKeyboard(IKeyboard kb) { kb.KeyDown += (_, k, _) => Root.OnKeyDown((int)k); kb.KeyUp += (_, k, _) => Root.OnKeyUp((int)k); kb.KeyChar += (_, c) => Root.OnChar(c); } private static UiMouseButton MapButton(MouseButton b) => b switch { MouseButton.Left => UiMouseButton.Left, MouseButton.Right => UiMouseButton.Right, MouseButton.Middle => UiMouseButton.Middle, _ => UiMouseButton.Left, }; public void Dispose() { TextRenderer.Dispose(); } }