using System; using System.IO; using System.Linq; using System.Text; using gaemstone.Client.Utility; using gaemstone.ECS; using gaemstone.Flecs; using gaemstone.Utility; using ImGuiNET; using Silk.NET.Input; using Silk.NET.OpenGL.Extensions.ImGui; using static gaemstone.Client.Components.InputComponents; using static gaemstone.Client.Systems.Windowing; namespace gaemstone.Client.Systems; [Module] [DependsOn] [DependsOn] [DependsOn] public class ImGuiManager { [Entity, Add] [DependsOn] public struct ImGuiUpdatePhase { } [Entity, Add] [DependsOn] public struct ImGuiRenderPhase { } [Component, Singleton(AutoAdd = false)] public class ImGuiData { public ImGuiController Controller { get; } internal ImGuiData(ImGuiController controller) => Controller = controller; } private unsafe static ImFontPtr AddFontFromResources( ImGuiIOPtr io, string name, int size, Action? cfgAction = null, float offset = 0, float minAdvance = 0, (int Min, int Max)[]? ranges = null, bool merge = false) { // TODO: FontConfig can be freed at the end of this method. // Unfortunately, data has to stick around until font atlas is built. var cfg = new ImFontConfigPtr(ImGuiNative.ImFontConfig_ImFontConfig()) { GlyphOffset = new(0, offset), GlyphMinAdvanceX = minAdvance, MergeMode = merge, }; // Set cfg.Name so the font has a nice display name in ImGui. var fullName = $"{name.Replace('.', ' ')} {size}px"; Encoding.UTF8.GetBytes(fullName, new Span(cfg.Name.Data, cfg.Name.Count)); // If glyph ranges are specified, allocate unmanaged heap memory for them. if (ranges != null) { var rangesSpan = GlobalHeapAllocator.Instance.Allocate(ranges.Length * 2 + 1); for (var i = 0; i < ranges.Length; i++) { rangesSpan[i * 2 ] = (char)ranges[i].Min; rangesSpan[i * 2 + 1] = (char)ranges[i].Max; } rangesSpan[^1] = default; fixed (void* rangesPtr = rangesSpan) cfg.GlyphRanges = (IntPtr)rangesPtr; } // Use cfgAction to allow changing other font configs. cfgAction?.Invoke(cfg); // Grab the stream for this font from the assembly's resources. var file = $"gaemstone.Client.Resources.{name}.ttf"; using var stream = typeof(Resources).Assembly.GetManifestResourceStream(file) ?? throw new InvalidOperationException($"Resource '{file}' was not found"); using var memoryStream = new MemoryStream(); stream.CopyTo(memoryStream); // Write font file from resources to memory stream. memoryStream.WriteByte(0); // Add a NUL termination character. // Copy the data into unmanaged memory and pass it to ImGui. var fontData = memoryStream.ToArray(); var fontDataSpan = GlobalHeapAllocator.Instance.AllocateCopy(fontData); fixed (byte* dataPtr = fontDataSpan) return io.Fonts.AddFontFromMemoryTTF((IntPtr)dataPtr, size, size, cfg); } [System] public unsafe void Initialize(Universe universe, GameWindow window, Canvas canvas, [Source] IInputContext inputContext, [Not] ImGuiData _) => universe.LookupByTypeOrThrow().Set(new ImGuiData( new(canvas.GL, window.Handle, inputContext, () => { var io = ImGui.GetIO(); var style = ImGui.GetStyle(); var fontSize = 16; void MergeIcons() => AddFontFromResources(io, "ForkAwesome", fontSize, minAdvance: 18, ranges: new[]{ (ForkAwesome.Min, ForkAwesome.Max) }, merge: true); AddFontFromResources(io, "OpenSans" , fontSize, offset: -1); MergeIcons(); AddFontFromResources(io, "OpenSans.Bold" , fontSize, offset: -1); MergeIcons(); AddFontFromResources(io, "OpenSans.Italic", fontSize, offset: -1); MergeIcons(); // Do not save a settings or log file. io.NativePtr->IniFilename = null; io.NativePtr->LogFilename = null; io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors | ImGuiBackendFlags.HasSetMousePos; io.ConfigFlags |= ImGuiConfigFlags.NavEnableKeyboard | ImGuiConfigFlags.NavEnableSetMousePos; // | ImGuiConfigFlags.DockingEnable; io.ConfigWindowsResizeFromEdges = true; io.ConfigWindowsMoveFromTitleBarOnly = true; style.WindowRounding = style.ChildRounding = style.PopupRounding = style.TabRounding = style.FrameRounding = style.ScrollbarRounding = 6; style.WindowTitleAlign = new(0.5f, 0.5f); style.ColorButtonPosition = ImGuiDir.Left; style.IndentSpacing = 8; // Set up key mappings. foreach (var imguiKey in Enum.GetValues()) { var name = imguiKey.ToString(); // Adjust ImGuiKey enum names to match Silk.NET.Input.Key enum. if (name.StartsWith("_")) name = name.Replace("_", "Number"); if (name.EndsWith("Arrow")) name = name[..^"Arrow".Length]; if (name.EndsWith("Ctrl" )) name = name.Replace("Ctrl", "Control"); if (!name.EndsWith("Bracket")) { // Leave "LeftBracket" and "RightBracket" alone. if (name.StartsWith("Left" )) name = name["Left" .Length..] + "Left"; if (name.StartsWith("Right")) name = name["Right".Length..] + "Right"; } if (Enum.TryParse(name, true, out var silkKey)) io.KeyMap[(int)imguiKey] = (int)silkKey; } }))); [System] public static void UpdateMouse(Universe universe, [Source] IMouse impl, ImGuiData _) { var input = universe.LookupByTypeOrThrow(); var module = universe.LookupByTypeOrThrow(); var capturedBy = input.GetTargets().FirstOrDefault();; var isCaptured = (capturedBy != null); // If another system has the mouse captured, don't do anything here. if (isCaptured && (capturedBy != module)) return; var IO = ImGui.GetIO(); // Set the mouse position if ImGui wants to move it. if (IO.WantSetMousePos) impl.Position = IO.MousePos; // Capture the mouse input it is above GUI elements. if (IO.WantCaptureMouse != isCaptured) { if (IO.WantCaptureMouse) input.Add (module); else input.Remove(module); } var cursor = ImGui.GetMouseCursor(); impl.Cursor.CursorMode = (cursor == ImGuiMouseCursor.None) ? CursorMode.Hidden : CursorMode.Normal; // TODO: Use additional cursors once Silk.NET supports GLFW 3.4. impl.Cursor.StandardCursor = cursor switch { ImGuiMouseCursor.Arrow => StandardCursor.Arrow, ImGuiMouseCursor.TextInput => StandardCursor.IBeam, // ImGuiMouseCursor.ResizeAll => StandardCursor., ImGuiMouseCursor.ResizeNS => StandardCursor.VResize, ImGuiMouseCursor.ResizeEW => StandardCursor.HResize, // ImGuiMouseCursor.ResizeNESW => StandardCursor., // ImGuiMouseCursor.ResizeNWSE => StandardCursor., ImGuiMouseCursor.Hand => StandardCursor.Hand, // ImGuiMouseCursor.NotAllowed => StandardCursor., _ => StandardCursor.Default, }; } [System] public static void Update(TimeSpan delta, ImGuiData imgui) => imgui.Controller.Update((float)delta.TotalSeconds); [System] public static void Render(ImGuiData imgui) => imgui.Controller.Render(); }