using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Hexa.NET.ImGui; using Hexa.NET.OpenGL; using SDL3; namespace SDL3_TestingSuite.SDL3; /// /// Implementation of SDL3 platform backend for ImGui. /// https://github.com/ocornut/imgui/blob/master/backends/imgui_impl_sdl3.h /// public unsafe static class ImGuiSDL3Platform { public class PlatformData { public nint Window; public uint WindowID; // public nint Renderer; public GL Gl = null!; public ulong Time; public nint ClipboardTextData; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 48)] public byte[] BackendPlatformName = new byte[48]; public bool UseVulkan; public bool WantUpdateMonitors; // IME Handling public nint ImeWindow; public ImGuiPlatformImeData ImeData; public bool ImeDirty; // Mouse Handling public uint MouseWindowID; public int MouseButtonsDown; public readonly nint[] MouseCursors = new nint[(int)ImGuiMouseCursor.Count]; public nint MouseLastCursor; public int MousePendingLeaveFrame; public bool MouseCanUseGlobalState; public bool MouseCanReportHoveredViewport; public MouseCaptureMode MouseCaptureMode; // Gamepad Handling public readonly nint[] Gamepads = new nint[16]; public int GamepadCount; public GamepadMode GamepadMode; public bool WantUpdateGamepadsList; public PlatformData() { MouseLastCursor = nint.Zero; for (int i = 0; i < MouseCursors.Length; i++) MouseCursors[i] = nint.Zero; for (int i = 0; i < Gamepads.Length; i++) Gamepads[i] = nint.Zero; } } public static PlatformData GetPlatformData() => ImGui.GetCurrentContext().Handle != null ? ImGuiUserData.Get(ImGui.GetIO().BackendPlatformUserData)! : null!; private static nint _glContext; public static bool Init(GL gl, nint window, nint ctx) { _glContext = ctx; ImGuiIOPtr io = ImGui.GetIO(); PlatformData data = new PlatformData { Window = window, WindowID = SDL.GetWindowID(window), Gl = gl }; io.BackendPlatformUserData = ImGuiUserData.Store(data); io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors; io.BackendFlags |= ImGuiBackendFlags.HasSetMousePos; data.MouseCanUseGlobalState = false; data.MouseCaptureMode = MouseCaptureMode.Disabled; // Check and store if we are on a SDL backend that supports SDL_GetGlobalMouseState() and SDL_CaptureMouse() // ("wayland" and "rpi" don't support it, but we chose to use a white-list instead of a black-list) string? backend = SDL.GetCurrentVideoDriver(); string[] captureAndGlobalStateWhitelist = { "windows", "cocoa", "x11", "DIVE", "VMAN" }; foreach (string item in captureAndGlobalStateWhitelist) { if (item != backend) continue; data.MouseCanUseGlobalState = true; data.MouseCaptureMode = item == "x11" ? MouseCaptureMode.EnabledAfterDrag : MouseCaptureMode.Enabled; } if (data.MouseCanUseGlobalState) { io.BackendFlags |= ImGuiBackendFlags.PlatformHasViewports; // io.BackendFlags |= ImGuiBackendFlags.HasParentViewport; } // SDL on Linux/OSX doesn't report events for unfocused windows (see https://github.com/ocornut/imgui/issues/4960) // We will use MouseCanReportHoveredViewport to set HasMouseHoveredViewport dynamically each frame. #if __APPLE__ data.MouseCanReportHoveredViewport = false; #else data.MouseCanReportHoveredViewport = data.MouseCanUseGlobalState; #endif ImGuiPlatformIOPtr platformIo = ImGui.GetPlatformIO(); platformIo.PlatformGetClipboardTextFn = DelegateStorage.GetClipboardDelegate.GetPtrForDelegate(); platformIo.PlatformSetClipboardTextFn = DelegateStorage.SetClipboardDelegate.GetPtrForDelegate(); platformIo.PlatformSetImeDataFn = DelegateStorage.SetImeDataDelegate.GetPtrForDelegate(); platformIo.PlatformOpenInShellFn = DelegateStorage.OpenInShellDelegate.GetPtrForDelegate(); UpdateMonitors(); data.GamepadMode = GamepadMode.AutoFirst; data.WantUpdateGamepadsList = true; data.MouseCursors[(int)ImGuiMouseCursor.Arrow] = SDL.CreateSystemCursor(SDL.SystemCursor.Default); data.MouseCursors[(int)ImGuiMouseCursor.TextInput] = SDL.CreateSystemCursor(SDL.SystemCursor.Text); data.MouseCursors[(int)ImGuiMouseCursor.ResizeAll] = SDL.CreateSystemCursor(SDL.SystemCursor.Move); data.MouseCursors[(int)ImGuiMouseCursor.ResizeNs] = SDL.CreateSystemCursor(SDL.SystemCursor.NSResize); data.MouseCursors[(int)ImGuiMouseCursor.ResizeEw] = SDL.CreateSystemCursor(SDL.SystemCursor.EWResize); data.MouseCursors[(int)ImGuiMouseCursor.ResizeNesw] = SDL.CreateSystemCursor(SDL.SystemCursor.NESWResize); data.MouseCursors[(int)ImGuiMouseCursor.ResizeNwse] = SDL.CreateSystemCursor(SDL.SystemCursor.NWSEResize); data.MouseCursors[(int)ImGuiMouseCursor.Hand] = SDL.CreateSystemCursor(SDL.SystemCursor.Pointer); data.MouseCursors[(int)ImGuiMouseCursor.Wait] = SDL.CreateSystemCursor(SDL.SystemCursor.Wait); data.MouseCursors[(int)ImGuiMouseCursor.Progress] = SDL.CreateSystemCursor(SDL.SystemCursor.Progress); data.MouseCursors[(int)ImGuiMouseCursor.NotAllowed] = SDL.CreateSystemCursor(SDL.SystemCursor.NotAllowed); SetupPlatformHandles(ImGui.GetMainViewport(), window); SDL.SetHint(SDL.Hints.MouseFocusClickthrough, "1"); SDL.SetHint(SDL.Hints.MouseAutoCapture, "0"); SDL.SetHint("SDL_BORDERLESS_WINDOWED_STYLE", "0"); if ((io.BackendFlags & ImGuiBackendFlags.PlatformHasViewports) != 0) InitMultiViewportSupport(ctx, window); return true; } public static void NewFrame() { ImGuiIOPtr io = ImGui.GetIO(); PlatformData data = GetPlatformData(); GetWindowSizeAndFramebufferScale(data.Window, out Vector2 size, out Vector2 framebufferScale); io.DisplaySize = size; io.DisplayFramebufferScale = framebufferScale; #if WIN32 data.WantUpdateMonitors = true; #endif if (data.WantUpdateMonitors) UpdateMonitors(); ulong frequency = SDL.GetPerformanceFrequency(); ulong currentTime = SDL.GetPerformanceCounter(); if (currentTime <= data.Time) currentTime = data.Time + 1; io.DeltaTime = data.Time > 0 ? (float)((currentTime - data.Time) / (double)frequency) : 1.0f / 60.0f; data.Time = currentTime; if (data.MousePendingLeaveFrame != 0 && data.MousePendingLeaveFrame >= ImGui.GetFrameCount() && data.MouseButtonsDown == 0) { data.MouseWindowID = 0; data.MousePendingLeaveFrame = 0; io.AddMousePosEvent(-float.MaxValue, -float.MaxValue); } if (data.MouseCanReportHoveredViewport && ImGui.GetDragDropPayload().IsNull) io.BackendFlags |= ImGuiBackendFlags.HasMouseHoveredViewport; else io.BackendFlags &= ~ImGuiBackendFlags.HasMouseHoveredViewport; UpdateMouseData(); UpdateMouseCursor(); UpdateIme(); UpdateGamepads(); } public static void UpdateMonitors() { PlatformData bd = GetPlatformData(); ImGuiPlatformIOPtr platformio = ImGui.GetPlatformIO(); platformio.Monitors.Resize(0); bd.WantUpdateMonitors = false; uint[] displays = SDL.GetDisplays(out int displayCount)!; for (int n = 0; n < displayCount; n++) { // Warning: the validity of monitor DPI information on Windows depends on the application DPI awareness settings, which generally needs to be set in the manifest or at runtime. uint displayID = displays[n]; ImGuiPlatformMonitor monitor = new ImGuiPlatformMonitor(); SDL.GetDisplayBounds(displayID, out SDL.Rect r); monitor.MainPos = monitor.WorkPos = new Vector2(r.X, r.Y); monitor.MainSize = monitor.WorkSize = new Vector2(r.W, r.H); if (SDL.GetDisplayUsableBounds(displayID, out SDL.Rect r2) && r2.W > 0 && r2.H > 0) { monitor.WorkPos = new Vector2(r2.X, r2.Y); monitor.WorkSize = new Vector2(r2.W, r2.H); } monitor.DpiScale = SDL.GetDisplayContentScale(displayID); // See https://wiki.libsdl.org/SDL3/README-highdpi for details. monitor.PlatformHandle = (void*)n; if (monitor.DpiScale <= 0.0f) continue; // Some accessibility applications are declaring virtual monitors with a DPI of 0, see #7902. platformio.Monitors.PushBack(monitor); } } private static void GetWindowSizeAndFramebufferScale(nint window, out Vector2 out_size, out Vector2 out_framebuffer_scale) { SDL.GetWindowSize(window, out int w, out int h); if (SDL.GetWindowFlags(window).HasFlag(SDL.WindowFlags.Minimized)) w = h = 0; #if __APPLE__ // SDL_GetWindowDisplayScale() seems more reliable during resolution changes e.g. going fullscreen (#8703, #4414) float fbScaleX = SDL.GetWindowDisplayScale(window); float fbScaleY = fbScaleX; #else SDL.GetWindowSizeInPixels(window, out int displayW, out int displayH); float fbScaleX = w > 0 ? displayW / (float)w : 1.0f; float fbScaleY = h > 0 ? displayH / (float)h : 1.0f; #endif out_size = new Vector2(w, h); out_framebuffer_scale = new Vector2(fbScaleX, fbScaleY); } public static bool ProcessEvent(SDL.Event e) { ImGuiIOPtr io = ImGui.GetIO(); PlatformData data = GetPlatformData(); switch ((SDL.EventType)e.Type) { case SDL.EventType.MouseMotion: { if (GetViewportForWindowId(e.Motion.WindowID) == null) return false; Vector2 mousePos = new Vector2(e.Motion.X, e.Motion.Y); if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable)) { SDL.GetWindowPosition(SDL.GetWindowFromID(e.Motion.WindowID), out int windowX, out int windowY); mousePos.X += windowX; mousePos.Y += windowY; } io.AddMouseSourceEvent(e.Motion.Which == SDL.TouchMouseID ? ImGuiMouseSource.TouchScreen : ImGuiMouseSource.Mouse); io.AddMousePosEvent(mousePos.X, mousePos.Y); return true; } case SDL.EventType.MouseWheel: { if (GetViewportForWindowId(e.Wheel.WindowID) == null) return false; float wheelX = -e.Wheel.X; float wheelY = e.Wheel.Y; io.AddMouseSourceEvent(e.Wheel.Which == SDL.TouchMouseID ? ImGuiMouseSource.TouchScreen : ImGuiMouseSource.Mouse); io.AddMouseWheelEvent(wheelX, wheelY); return true; } case SDL.EventType.MouseButtonDown: case SDL.EventType.MouseButtonUp: { if (GetViewportForWindowId(e.Button.WindowID) == null) return false; int mouseButton = e.Button.Button switch { SDL.ButtonLeft => 0, SDL.ButtonRight => 1, SDL.ButtonMiddle => 2, SDL.ButtonX1 => 3, SDL.ButtonX2 => 4, _ => -1 }; if (mouseButton == -1) break; io.AddMouseSourceEvent(e.Button.Which == SDL.TouchMouseID ? ImGuiMouseSource.TouchScreen : ImGuiMouseSource.Mouse); io.AddMouseButtonEvent(mouseButton, (SDL.EventType)e.Type == SDL.EventType.MouseButtonDown); data.MouseButtonsDown = (SDL.EventType)e.Type == SDL.EventType.MouseButtonDown ? data.MouseButtonsDown | 1 << mouseButton : data.MouseButtonsDown & ~(1 << mouseButton); return true; } case SDL.EventType.TextInput: { if (GetViewportForWindowId(e.Text.WindowID) == null) return false; io.AddInputCharactersUTF8((byte*)e.Text.Text); return true; } case SDL.EventType.KeyDown: case SDL.EventType.KeyUp: { if (GetViewportForWindowId(e.Key.WindowID) == null) return false; UpdateKeyModifiers(e.Key.Mod); ImGuiKey imguiKey = KeyEventToImGui(e.Key.Key, e.Key.Scancode); bool pressed = (SDL.EventType)e.Type == SDL.EventType.KeyDown; io.AddKeyEvent(imguiKey, pressed); io.SetKeyEventNativeData(imguiKey, (int)e.Key.Key, (int)e.Key.Scancode, (int)e.Key.Scancode); return true; } case SDL.EventType.DisplayOrientation: case SDL.EventType.DisplayAdded: case SDL.EventType.DisplayRemoved: case SDL.EventType.DisplayMoved: case SDL.EventType.DisplayContentScaleChanged: // SDL_HAS_EVENT_DISPLAY_USABLE_BOUNDS_CHANGED (SDL 3.4.0+): add DisplayUsableBoundsChanged here when available { data.WantUpdateMonitors = true; return true; } case SDL.EventType.WindowMouseEnter: { if (GetViewportForWindowId(e.Window.WindowID) == null) return false; data.MouseWindowID = e.Window.WindowID; data.MousePendingLeaveFrame = 0; return true; } // In some cases, when detaching a window from main viewport SDL may send SDL_WINDOWEVENT_ENTER one frame // too late, causing SDL_WINDOWEVENT_LEAVE on previous frame to interrupt drag operation by clearing mouse // position. This is why we delay processing the SDL_WINDOWEVENT_LEAVE events by one frame. // See issue #5012 for details. FIXME: Unconfirmed whether this is still needed with SDL3. case SDL.EventType.WindowMouseLeave: { if (GetViewportForWindowId(e.Window.WindowID) == null) return false; data.MousePendingLeaveFrame = ImGui.GetFrameCount() + 1; return true; } case SDL.EventType.WindowFocusGained: case SDL.EventType.WindowFocusLost: { if (GetViewportForWindowId(e.Window.WindowID) == null) return false; io.AddFocusEvent((SDL.EventType)e.Type == SDL.EventType.WindowFocusGained); return true; } case SDL.EventType.WindowCloseRequested: case SDL.EventType.WindowMoved: case SDL.EventType.WindowResized: { ImGuiViewportPtr? viewport = GetViewportForWindowId(e.Window.WindowID); if (viewport == null) return false; if ((SDL.EventType)e.Type == SDL.EventType.WindowCloseRequested) viewport.Value.PlatformRequestClose = true; if ((SDL.EventType)e.Type == SDL.EventType.WindowMoved) viewport.Value.PlatformRequestMove = true; if ((SDL.EventType)e.Type == SDL.EventType.WindowResized) viewport.Value.PlatformRequestResize = true; return true; } case SDL.EventType.GamepadAdded: case SDL.EventType.GamepadRemoved: { data.WantUpdateGamepadsList = true; return true; } } return false; } // This code is rather messy because some of the functions we need for full viewport support are not available in SDL < 2.0.4. internal static void UpdateMouseData() { PlatformData bd = GetPlatformData(); ImGuiIOPtr io = ImGui.GetIO(); // We forward mouse input when hovered or captured (via SDL_EVENT_MOUSE_MOTION) or when focused (below). // SDL_CaptureMouse() lets the OS know e.g. that our drags can extend outside of parent boundaries (we want // updated position) and shouldn't trigger other operations outside. // Debuggers under Linux tend to leave captured mouse on break, which may be very inconvenient, so to mitigate // the issue on X11 we wait until mouse has moved to begin capture. if (bd.MouseCaptureMode == MouseCaptureMode.Enabled) { SDL.CaptureMouse(bd.MouseButtonsDown != 0); } else if (bd.MouseCaptureMode == MouseCaptureMode.EnabledAfterDrag) { bool wantCapture = false; for (int buttonN = 0; buttonN < (int)ImGuiMouseButton.Count && !wantCapture; buttonN++) { if (ImGui.IsMouseDragging((ImGuiMouseButton)buttonN, 1.0f)) wantCapture = true; } SDL.CaptureMouse(wantCapture); } nint focusedWindow = SDL.GetKeyboardFocus(); bool isAppFocused = focusedWindow != nint.Zero && (bd.Window == focusedWindow || GetViewportForWindowId(SDL.GetWindowID(focusedWindow)) != null); if (isAppFocused) { // (Optional) Set OS mouse position from Dear ImGui if requested (rarely used, only when // io.ConfigNavMoveSetMousePos is enabled by user) if (io.WantSetMousePos) { if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable)) SDL.WarpMouseGlobal(io.MousePos.X, io.MousePos.Y); else SDL.WarpMouseInWindow(bd.Window, io.MousePos.X, io.MousePos.Y); } // (Optional) Fallback to provide unclamped mouse position when focused but not hovered // (SDL_EVENT_MOUSE_MOTION already provides this when hovered or captured) // Note that SDL_GetGlobalMouseState() is in theory slow on X11, but this only runs on rather specific // cases. If a problem we may provide a way to opt-out this feature. nint hoveredWindow = SDL.GetMouseFocus(); bool isRelativeMouseMode = SDL.GetWindowRelativeMouseMode(bd.Window); if (hoveredWindow == nint.Zero && bd.MouseCanUseGlobalState && bd.MouseButtonsDown == 0 && !isRelativeMouseMode) { // Single-viewport mode: mouse position in client window coordinates // (io.MousePos is (0,0) when the mouse is on the upper-left corner of the app window) // Multi-viewport mode: mouse position in OS absolute coordinates // (io.MousePos is (0,0) when the mouse is on the upper-left of the primary monitor) SDL.GetGlobalMouseState(out float mouseX, out float mouseY); if (!io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable)) { SDL.GetWindowPosition(focusedWindow, out int windowX, out int windowY); mouseX -= windowX; mouseY -= windowY; } io.AddMousePosEvent(mouseX, mouseY); } } // (Optional) When using multiple viewports: call io.AddMouseViewportEvent() with the viewport the OS mouse // cursor is hovering. If ImGuiBackendFlags_HasMouseHoveredViewport is not set by the backend, Dear ImGui will // ignore this field and infer the information using its flawed heuristic. // - [!] SDL backend does NOT correctly ignore viewports with the _NoInputs flag. // Some backends are not able to handle that correctly. If a backend reports a hovered viewport that has // the _NoInputs flag (e.g. when dragging a window for docking, the viewport has the _NoInputs flag in // order to allow us to find the viewport under), then Dear ImGui is forced to ignore the value reported // by the backend, and use its flawed heuristic to guess the viewport behind. // - [X] SDL backend correctly reports this regardless of another viewport behind focused and dragged from // (we need this to find a useful drag and drop target). if (io.BackendFlags.HasFlag(ImGuiBackendFlags.HasMouseHoveredViewport)) { uint mouseViewportID = 0; ImGuiViewportPtr? mouseViewport = GetViewportForWindowId(bd.MouseWindowID); if (mouseViewport != null && !mouseViewport.Value.IsNull) mouseViewportID = mouseViewport.Value.ID; io.AddMouseViewportEvent(mouseViewportID); } } internal static void UpdateMouseCursor() { ImGuiIOPtr io = ImGui.GetIO(); if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.NoMouseCursorChange)) return; PlatformData bd = GetPlatformData(); ImGuiMouseCursor imguiCursor = ImGui.GetMouseCursor(); if (io.MouseDrawCursor || imguiCursor == ImGuiMouseCursor.None) { // Hide OS mouse cursor if imgui is drawing it or if it wants no cursor SDL.HideCursor(); } else { // Show OS mouse cursor nint expectedCursor = bd.MouseCursors[(int)imguiCursor] != nint.Zero ? bd.MouseCursors[(int)imguiCursor] : bd.MouseCursors[(int)ImGuiMouseCursor.Arrow]; if (bd.MouseLastCursor != expectedCursor) { SDL.SetCursor(expectedCursor); // SDL function doesn't have an early out (see #6113) bd.MouseLastCursor = expectedCursor; } SDL.ShowCursor(); } } internal static void UpdateGamepads() { PlatformData data = GetPlatformData(); ImGuiIOPtr io = ImGui.GetIO(); if (data.WantUpdateGamepadsList && data.GamepadMode != GamepadMode.Manual) { CloseGamepads(); uint[] sdlGamepads = SDL.GetGamepads(out int count)!; for (int n = 0; n < count; n++) { if (data.GamepadCount >= data.Gamepads.Length) break; nint gamepad = SDL.OpenGamepad(sdlGamepads[n]); if (gamepad != nint.Zero) { data.Gamepads[data.GamepadCount++] = gamepad; if (data.GamepadMode == GamepadMode.AutoFirst) break; } } data.WantUpdateGamepadsList = false; } io.BackendFlags &= ~ImGuiBackendFlags.HasGamepad; if (data.GamepadCount == 0) return; io.BackendFlags |= ImGuiBackendFlags.HasGamepad; // Update gamepad inputs const int thumbDeadZone = 8000; // SDL_gamepad.h suggests using this value. UpdateGamepadButton(data, io, ImGuiKey.GamepadStart, SDL.GamepadButton.Start); UpdateGamepadButton(data, io, ImGuiKey.GamepadBack, SDL.GamepadButton.Back); UpdateGamepadButton(data, io, ImGuiKey.GamepadFaceLeft, SDL.GamepadButton.West); // Xbox X, PS Square UpdateGamepadButton(data, io, ImGuiKey.GamepadFaceRight, SDL.GamepadButton.East); // Xbox B, PS Circle UpdateGamepadButton(data, io, ImGuiKey.GamepadFaceUp, SDL.GamepadButton.North); // Xbox Y, PS Triangle UpdateGamepadButton(data, io, ImGuiKey.GamepadFaceDown, SDL.GamepadButton.South); // Xbox A, PS Cross UpdateGamepadButton(data, io, ImGuiKey.GamepadDpadLeft, SDL.GamepadButton.DPadLeft); UpdateGamepadButton(data, io, ImGuiKey.GamepadDpadRight, SDL.GamepadButton.DPadRight); UpdateGamepadButton(data, io, ImGuiKey.GamepadDpadUp, SDL.GamepadButton.DPadUp); UpdateGamepadButton(data, io, ImGuiKey.GamepadDpadDown, SDL.GamepadButton.DPadDown); UpdateGamepadButton(data, io, ImGuiKey.GamepadL1, SDL.GamepadButton.LeftShoulder); UpdateGamepadButton(data, io, ImGuiKey.GamepadR1, SDL.GamepadButton.RightShoulder); UpdateGamepadAnalog(data, io, ImGuiKey.GamepadL2, SDL.GamepadAxis.LeftTrigger, 0.0f, 32767); UpdateGamepadAnalog(data, io, ImGuiKey.GamepadR2, SDL.GamepadAxis.RightTrigger, 0.0f, 32767); UpdateGamepadButton(data, io, ImGuiKey.GamepadL3, SDL.GamepadButton.LeftStick); UpdateGamepadButton(data, io, ImGuiKey.GamepadR3, SDL.GamepadButton.RightStick); UpdateGamepadAnalog(data, io, ImGuiKey.GamepadLStickLeft, SDL.GamepadAxis.LeftX, -thumbDeadZone, -32768); UpdateGamepadAnalog(data, io, ImGuiKey.GamepadLStickRight, SDL.GamepadAxis.LeftX, +thumbDeadZone, +32767); UpdateGamepadAnalog(data, io, ImGuiKey.GamepadLStickUp, SDL.GamepadAxis.LeftY, -thumbDeadZone, -32768); UpdateGamepadAnalog(data, io, ImGuiKey.GamepadLStickDown, SDL.GamepadAxis.LeftY, +thumbDeadZone, +32767); UpdateGamepadAnalog(data, io, ImGuiKey.GamepadRStickLeft, SDL.GamepadAxis.RightX, -thumbDeadZone, -32768); UpdateGamepadAnalog(data, io, ImGuiKey.GamepadRStickRight, SDL.GamepadAxis.RightX, +thumbDeadZone, +32767); UpdateGamepadAnalog(data, io, ImGuiKey.GamepadRStickUp, SDL.GamepadAxis.RightY, -thumbDeadZone, -32768); UpdateGamepadAnalog(data, io, ImGuiKey.GamepadRStickDown, SDL.GamepadAxis.RightY, +thumbDeadZone, +32767); } private static void UpdateGamepadButton(PlatformData bd, ImGuiIOPtr io, ImGuiKey key, SDL.GamepadButton button_no) { bool mergedValue = false; for (int i = 0; i < bd.GamepadCount; i++) mergedValue |= SDL.GetGamepadButton(bd.Gamepads[i], button_no); io.AddKeyEvent(key, mergedValue); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static float Saturate(float v) => v < 0.0f ? 0.0f : v > 1.0f ? 1.0f : v; private static void UpdateGamepadAnalog(PlatformData bd, ImGuiIOPtr io, ImGuiKey key, SDL.GamepadAxis axis_no, float v0, float v1) { float mergedValue = 0.0f; for (int i = 0; i < bd.GamepadCount; i++) { float vn = Saturate((SDL.GetGamepadAxis(bd.Gamepads[i], axis_no) - v0) / (v1 - v0)); if (mergedValue < vn) mergedValue = vn; } io.AddKeyAnalogEvent(key, mergedValue > 0.1f, mergedValue); } internal static void SetGamepadMode(GamepadMode mode, nint[] manual_gamepads_array, int manual_gamepads_count) { PlatformData bd = GetPlatformData(); CloseGamepads(); if (mode == GamepadMode.Manual) { int count = Math.Min(manual_gamepads_count, bd.Gamepads.Length); for (int n = 0; n < count; n++) bd.Gamepads[n] = manual_gamepads_array[n]; bd.GamepadCount = count; } else { bd.WantUpdateGamepadsList = true; } bd.GamepadMode = mode; } internal static void CloseGamepads() { PlatformData bd = GetPlatformData(); if (bd.GamepadMode != GamepadMode.Manual) { for (int i = 0; i < bd.GamepadCount; i++) { SDL.CloseGamepad(bd.Gamepads[i]); bd.Gamepads[i] = nint.Zero; } } bd.GamepadCount = 0; } // Not static to allow third-party code to use that if they want to (but undocumented) public static ImGuiKey KeyEventToImGui(SDL.Keycode keycode, SDL.Scancode scancode) { // Keypad doesn't have individual key values in SDL3 — always use scancode for these. switch (scancode) { case SDL.Scancode.Kp0: return ImGuiKey.Keypad0; case SDL.Scancode.Kp1: return ImGuiKey.Keypad1; case SDL.Scancode.Kp2: return ImGuiKey.Keypad2; case SDL.Scancode.Kp3: return ImGuiKey.Keypad3; case SDL.Scancode.Kp4: return ImGuiKey.Keypad4; case SDL.Scancode.Kp5: return ImGuiKey.Keypad5; case SDL.Scancode.Kp6: return ImGuiKey.Keypad6; case SDL.Scancode.Kp7: return ImGuiKey.Keypad7; case SDL.Scancode.Kp8: return ImGuiKey.Keypad8; case SDL.Scancode.Kp9: return ImGuiKey.Keypad9; case SDL.Scancode.KpPeriod: return ImGuiKey.KeypadDecimal; case SDL.Scancode.KpDivide: return ImGuiKey.KeypadDivide; case SDL.Scancode.KpMultiply: return ImGuiKey.KeypadMultiply; case SDL.Scancode.KpMinus: return ImGuiKey.KeypadSubtract; case SDL.Scancode.KpPlus: return ImGuiKey.KeypadAdd; case SDL.Scancode.KpEnter: return ImGuiKey.KeypadEnter; case SDL.Scancode.KpEquals: return ImGuiKey.KeypadEqual; } // For regular keys, prefer keycode (layout-aware) but fall back to scancode for OEM/symbol keys. // This matches the C++ behaviour where OEM keys are commented out of the keycode switch and instead // handled by the scancode fallback below. switch (keycode) { case SDL.Keycode.Tab: return ImGuiKey.Tab; case SDL.Keycode.Left: return ImGuiKey.LeftArrow; case SDL.Keycode.Right: return ImGuiKey.RightArrow; case SDL.Keycode.Up: return ImGuiKey.UpArrow; case SDL.Keycode.Down: return ImGuiKey.DownArrow; case SDL.Keycode.Pageup: return ImGuiKey.PageUp; case SDL.Keycode.Pagedown: return ImGuiKey.PageDown; case SDL.Keycode.Home: return ImGuiKey.Home; case SDL.Keycode.End: return ImGuiKey.End; case SDL.Keycode.Insert: return ImGuiKey.Insert; case SDL.Keycode.Delete: return ImGuiKey.Delete; case SDL.Keycode.Backspace: return ImGuiKey.Backspace; case SDL.Keycode.Space: return ImGuiKey.Space; case SDL.Keycode.Return: return ImGuiKey.Enter; case SDL.Keycode.Escape: return ImGuiKey.Escape; // OEM keys intentionally omitted here — handled by scancode fallback below. // (Apostrophe, Minus, Slash, Equals, LeftBracket, Backslash, RightBracket, Grave) case SDL.Keycode.Comma: return ImGuiKey.Comma; case SDL.Keycode.Period: return ImGuiKey.Period; case SDL.Keycode.Semicolon: return ImGuiKey.Semicolon; case SDL.Keycode.Capslock: return ImGuiKey.CapsLock; case SDL.Keycode.ScrollLock: return ImGuiKey.ScrollLock; case SDL.Keycode.NumLockClear: return ImGuiKey.NumLock; case SDL.Keycode.PrintScreen: return ImGuiKey.PrintScreen; case SDL.Keycode.Pause: return ImGuiKey.Pause; case SDL.Keycode.LCtrl: return ImGuiKey.LeftCtrl; case SDL.Keycode.LShift: return ImGuiKey.LeftShift; case SDL.Keycode.LAlt: return ImGuiKey.LeftAlt; case SDL.Keycode.LGui: return ImGuiKey.LeftSuper; case SDL.Keycode.RCtrl: return ImGuiKey.RightCtrl; case SDL.Keycode.RShift: return ImGuiKey.RightShift; case SDL.Keycode.RAlt: return ImGuiKey.RightAlt; case SDL.Keycode.RGUI: return ImGuiKey.RightSuper; case SDL.Keycode.Application: return ImGuiKey.Menu; case SDL.Keycode.Alpha0: return ImGuiKey.Key0; case SDL.Keycode.Alpha1: return ImGuiKey.Key1; case SDL.Keycode.Alpha2: return ImGuiKey.Key2; case SDL.Keycode.Alpha3: return ImGuiKey.Key3; case SDL.Keycode.Alpha4: return ImGuiKey.Key4; case SDL.Keycode.Alpha5: return ImGuiKey.Key5; case SDL.Keycode.Alpha6: return ImGuiKey.Key6; case SDL.Keycode.Alpha7: return ImGuiKey.Key7; case SDL.Keycode.Alpha8: return ImGuiKey.Key8; case SDL.Keycode.Alpha9: return ImGuiKey.Key9; case SDL.Keycode.A: return ImGuiKey.A; case SDL.Keycode.B: return ImGuiKey.B; case SDL.Keycode.C: return ImGuiKey.C; case SDL.Keycode.D: return ImGuiKey.D; case SDL.Keycode.E: return ImGuiKey.E; case SDL.Keycode.F: return ImGuiKey.F; case SDL.Keycode.G: return ImGuiKey.G; case SDL.Keycode.H: return ImGuiKey.H; case SDL.Keycode.I: return ImGuiKey.I; case SDL.Keycode.J: return ImGuiKey.J; case SDL.Keycode.K: return ImGuiKey.K; case SDL.Keycode.L: return ImGuiKey.L; case SDL.Keycode.M: return ImGuiKey.M; case SDL.Keycode.N: return ImGuiKey.N; case SDL.Keycode.O: return ImGuiKey.O; case SDL.Keycode.P: return ImGuiKey.P; case SDL.Keycode.Q: return ImGuiKey.Q; case SDL.Keycode.R: return ImGuiKey.R; case SDL.Keycode.S: return ImGuiKey.S; case SDL.Keycode.T: return ImGuiKey.T; case SDL.Keycode.U: return ImGuiKey.U; case SDL.Keycode.V: return ImGuiKey.V; case SDL.Keycode.W: return ImGuiKey.W; case SDL.Keycode.X: return ImGuiKey.X; case SDL.Keycode.Y: return ImGuiKey.Y; case SDL.Keycode.Z: return ImGuiKey.Z; case SDL.Keycode.F1: return ImGuiKey.F1; case SDL.Keycode.F2: return ImGuiKey.F2; case SDL.Keycode.F3: return ImGuiKey.F3; case SDL.Keycode.F4: return ImGuiKey.F4; case SDL.Keycode.F5: return ImGuiKey.F5; case SDL.Keycode.F6: return ImGuiKey.F6; case SDL.Keycode.F7: return ImGuiKey.F7; case SDL.Keycode.F8: return ImGuiKey.F8; case SDL.Keycode.F9: return ImGuiKey.F9; case SDL.Keycode.F10: return ImGuiKey.F10; case SDL.Keycode.F11: return ImGuiKey.F11; case SDL.Keycode.F12: return ImGuiKey.F12; case SDL.Keycode.F13: return ImGuiKey.F13; case SDL.Keycode.F14: return ImGuiKey.F14; case SDL.Keycode.F15: return ImGuiKey.F15; case SDL.Keycode.F16: return ImGuiKey.F16; case SDL.Keycode.F17: return ImGuiKey.F17; case SDL.Keycode.F18: return ImGuiKey.F18; case SDL.Keycode.F19: return ImGuiKey.F19; case SDL.Keycode.F20: return ImGuiKey.F20; case SDL.Keycode.F21: return ImGuiKey.F21; case SDL.Keycode.F22: return ImGuiKey.F22; case SDL.Keycode.F23: return ImGuiKey.F23; case SDL.Keycode.F24: return ImGuiKey.F24; case SDL.Keycode.AcBack: return ImGuiKey.AppBack; case SDL.Keycode.AcForward: return ImGuiKey.AppForward; } // Fallback to scancode for OEM/symbol keys so non-US keyboard layouts still work (#7136, #7201, #7306). switch (scancode) { case SDL.Scancode.Grave: return ImGuiKey.GraveAccent; case SDL.Scancode.Minus: return ImGuiKey.Minus; case SDL.Scancode.Equals: return ImGuiKey.Equal; case SDL.Scancode.Leftbracket: return ImGuiKey.LeftBracket; case SDL.Scancode.Rightbracket: return ImGuiKey.RightBracket; case SDL.Scancode.NonUsbackslash: return ImGuiKey.Oem102; case SDL.Scancode.Backslash: return ImGuiKey.Backslash; case SDL.Scancode.Semicolon: return ImGuiKey.Semicolon; case SDL.Scancode.Apostrophe: return ImGuiKey.Apostrophe; case SDL.Scancode.Comma: return ImGuiKey.Comma; case SDL.Scancode.Period: return ImGuiKey.Period; case SDL.Scancode.Slash: return ImGuiKey.Slash; } return ImGuiKey.None; } public static void UpdateIme() { PlatformData data = GetPlatformData(); ImGuiPlatformImeData imeData = data.ImeData; nint window = SDL.GetKeyboardFocus(); // Stop previous input if ((!(imeData.WantVisible == 1 || imeData.WantTextInput == 1) || data.ImeWindow != window) && data.ImeWindow != nint.Zero) { SDL.StopTextInput(data.ImeWindow); data.ImeWindow = nint.Zero; } if (!data.ImeDirty && data.ImeWindow == window || window == nint.Zero) return; // Start/update current input data.ImeDirty = false; if (imeData.WantVisible == 1) { // Offset the IME input area by the viewport position so it tracks the correct screen location // when multiple viewports are in use. Vector2 viewportPos = Vector2.Zero; ImGuiViewportPtr? viewport = GetViewportForWindowId(SDL.GetWindowID(window)); if (viewport != null && !viewport.Value.IsNull) viewportPos = viewport.Value.Pos; SDL.Rect r = new SDL.Rect { X = (int)(imeData.InputPos.X - viewportPos.X), Y = (int)(imeData.InputPos.Y - viewportPos.Y), W = 1, H = (int)imeData.InputLineHeight }; SDL.SetTextInputArea(window, r, 0); data.ImeWindow = window; } if (!SDL.TextInputActive(window) && (imeData.WantVisible == 1 || imeData.WantTextInput == 1)) SDL.StartTextInput(window); } private static void UpdateKeyModifiers(SDL.Keymod mod) { ImGuiIOPtr io = ImGui.GetIO(); io.AddKeyEvent(ImGuiKey.ModCtrl, (mod & SDL.Keymod.Ctrl) != 0); io.AddKeyEvent(ImGuiKey.ModShift, (mod & SDL.Keymod.Shift) != 0); io.AddKeyEvent(ImGuiKey.ModAlt, (mod & SDL.Keymod.Alt) != 0); io.AddKeyEvent(ImGuiKey.ModSuper, (mod & SDL.Keymod.GUI) != 0); } public class ViewPortData { public nint Window; public nint ParentWindow; public uint WindowID; // Stored in ImGuiViewport.PlatformHandle. Use SDL.GetWindowFromID() to get SDL_Window* from Uint32 WindowID. public bool WindowOwned; public nint GLContext; } private static void InitMultiViewportSupport(nint glContext, nint window) { ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO(); platformIO.PlatformCreateWindow = DelegateStorage.CreateWindowDelegate.GetPtrForDelegate(); platformIO.PlatformDestroyWindow = DelegateStorage.DestroyWindowDelegate.GetPtrForDelegate(); platformIO.PlatformShowWindow = DelegateStorage.ShowWindowDelegate.GetPtrForDelegate(); platformIO.PlatformSetWindowPos = DelegateStorage.SetWindowPosDelegate.GetPtrForDelegate(); platformIO.PlatformGetWindowPos = DelegateStorage.GetWindowPosDelegate.GetPtrForDelegate(); platformIO.PlatformSetWindowSize = DelegateStorage.SetWindowSizeDelegate.GetPtrForDelegate(); platformIO.PlatformGetWindowSize = DelegateStorage.GetWindowSizeDelegate.GetPtrForDelegate(); platformIO.PlatformGetWindowFramebufferScale = DelegateStorage.GetWindowFramebufferScaleDelegate.GetPtrForDelegate(); platformIO.PlatformSetWindowFocus = DelegateStorage.SetWindowFocusDelegate.GetPtrForDelegate(); platformIO.PlatformGetWindowFocus = DelegateStorage.GetWindowFocusDelegate.GetPtrForDelegate(); platformIO.PlatformGetWindowMinimized = DelegateStorage.GetWindowMinimizedDelegate.GetPtrForDelegate(); platformIO.PlatformSetWindowTitle = DelegateStorage.SetWindowTitleDelegate.GetPtrForDelegate(); platformIO.PlatformSetWindowAlpha = DelegateStorage.SetWindowAlphaDelegate.GetPtrForDelegate(); platformIO.PlatformUpdateWindow = DelegateStorage.UpdateWindowDelegate.GetPtrForDelegate(); platformIO.PlatformRenderWindow = DelegateStorage.RenderWindowDelegate.GetPtrForDelegate(); platformIO.PlatformSwapBuffers = DelegateStorage.SwapBuffersDelegate.GetPtrForDelegate(); // Register main window handle (owned by the main application, not by us). // This is mostly for simplicity and consistency so that our code (e.g. mouse handling) can use // the same logic for main and secondary viewports. ImGuiViewportPtr mainViewport = ImGui.GetMainViewport(); ViewPortData vd = new ViewPortData { Window = window, WindowID = SDL.GetWindowID(window), WindowOwned = false, GLContext = glContext }; mainViewport.PlatformUserData = ImGuiUserData.Store(vd); mainViewport.PlatformHandle = (void*)(nint)vd.WindowID; #if WINDOWS mainViewport.PlatformHandleRaw = SDL.GetPointerProperty( SDL.GetWindowProperties(window), SDL.Properties.WindowWin32HWNDPointer, nint.Zero ).ToPointer(); #endif } public static ImGuiViewportPtr? GetViewportForWindowId(uint id) { return ImGui.FindViewportByPlatformHandle((void*)(nint)id); } private static nint GetSDLWindowFromViewport(ImGuiViewportPtr viewport) { return SDL.GetWindowFromID((uint)(nint)viewport.PlatformHandle); } private static void SetupPlatformHandles(ImGuiViewportPtr viewport, nint window) { viewport.PlatformHandle = (void*)(nint)SDL.GetWindowID(window); viewport.PlatformHandleRaw = (void*)nint.Zero; #if _WIN32 && !__WINTR__ viewport.PlatformHandleRaw = SDL.GetPointerProperty(SDL.GetWindowProperties(window), SDL.Props.WindowWin32HWNDPointer, 0).ToPointer(); #elif __APPLE__ && SDL_VIDEO_DRIVER_COCOA viewport.PlatformHandleRaw = SDL.GetPointerProperty(SDL.GetWindowProperties(window), SDL.Props.WindowCocoaWindow, 0).ToPointer(); #endif } public static void Dispose() { PlatformData? data = GetPlatformData(); if (data != null) { if (data.ClipboardTextData != nint.Zero) { Marshal.FreeHGlobal(data.ClipboardTextData); data.ClipboardTextData = nint.Zero; } foreach (nint cursor in data.MouseCursors) { if (cursor != nint.Zero) SDL.DestroyCursor(cursor); } CloseGamepads(); } ImGui.DestroyPlatformWindows(); ImGuiIOPtr io = ImGui.GetIO(); ImGuiPlatformIOPtr platformIo = ImGui.GetPlatformIO(); platformIo.PlatformGetClipboardTextFn = null; platformIo.PlatformSetClipboardTextFn = null; platformIo.PlatformSetImeDataFn = null; platformIo.PlatformOpenInShellFn = null; io.BackendFlags &= ~(ImGuiBackendFlags.HasMouseCursors | ImGuiBackendFlags.HasSetMousePos | ImGuiBackendFlags.HasGamepad | ImGuiBackendFlags.PlatformHasViewports | ImGuiBackendFlags.HasMouseHoveredViewport); ImGuiUserData.Free(io.BackendPlatformUserData); io.BackendPlatformUserData = null; } // @formatter:off — disable formatter after this line public static class DelegateStorage { [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate nint PlatformGetClipboardTextFn(nint ctx); internal static readonly PlatformGetClipboardTextFn GetClipboardDelegate = GetClipboardText; private static nint GetClipboardText(nint ctx) { PlatformData data = GetPlatformData(); if (data.ClipboardTextData != nint.Zero) { Marshal.FreeHGlobal(data.ClipboardTextData); data.ClipboardTextData = nint.Zero; } // Return nullptr on error (clipboard contents not text), consistent with other backends (#9168). string? text = SDL.HasClipboardText() ? SDL.GetClipboardText() : null; if (text == null) return nint.Zero; data.ClipboardTextData = Marshal.StringToHGlobalAnsi(text); return data.ClipboardTextData; } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void PlatformSetClipboardTextFn(nint ctx, nint text); internal static readonly PlatformSetClipboardTextFn SetClipboardDelegate = SetClipboardText; private static void SetClipboardText(nint ctx, nint text) { string? managedText = Marshal.PtrToStringAnsi(text); if (managedText != null) SDL.SetClipboardText(managedText); } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void PlatformSetImeDataFn(nint ctx, nint userData, nint imeData); internal static readonly PlatformSetImeDataFn SetImeDataDelegate = SetImeData; private static void SetImeData(nint ctx, nint userData, nint imeData) { PlatformData data = GetPlatformData(); data.ImeData = Marshal.PtrToStructure(imeData); data.ImeDirty = true; UpdateIme(); } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate bool PlatformOpenInShellFn(nint ctx, nint url); internal static readonly PlatformOpenInShellFn OpenInShellDelegate = OpenInShell; private static bool OpenInShell(nint ctx, nint url) { string managedUrl = Marshal.PtrToStringUTF8(url) ?? string.Empty; return SDL.OpenURL(managedUrl); } // // // // // // // // // // // // // // // Below is for Multi-Viewport Support // // // // // // // // // // // // // // // // // [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void ViewportCallback(ImGuiViewportPtr ctx); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void ViewportCallbackSetVec2(ImGuiViewportPtr ctx, Vector2 vec); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate Vector2 ViewportCallbackGetVec2(ImGuiViewportPtr ctx); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate bool ViewportCallbackBool(ImGuiViewportPtr ctx); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void ViewportCallbackSetString(ImGuiViewportPtr ctx, string title); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void ViewportCallbackSetFloat(ImGuiViewportPtr ctx, float alpha); internal static readonly ViewportCallback CreateWindowDelegate = CreateWindow; private static void CreateWindow(ImGuiViewportPtr viewport) { PlatformData bd = GetPlatformData(); ViewPortData vd = new ViewPortData(); viewport.PlatformUserData = ImGuiUserData.Store(vd); // Resolve parent window vd.ParentWindow = nint.Zero; if (viewport.ParentViewportId != 0) { ImGuiViewportPtr? parentViewport = ImGui.FindViewportByID(viewport.ParentViewportId); if (parentViewport != null && parentViewport.Value.Handle != null) vd.ParentWindow = GetSDLWindowFromViewport(parentViewport.Value); } bool useOpengl = _glContext != nint.Zero; nint backupContext = nint.Zero; if (useOpengl) { backupContext = SDL.GLGetCurrentContext(); SDL.GLSetAttribute(SDL.GLAttr.ShareWithCurrentContext, 1); // Make the main context current before creating the shared context ViewPortData? mainVd = ImGuiUserData.Get(ImGui.GetMainViewport().PlatformUserData); if (mainVd != null) SDL.GLMakeCurrent(mainVd.Window, mainVd.GLContext); } SDL.WindowFlags flags = SDL.WindowFlags.Hidden; if (useOpengl) flags |= SDL.WindowFlags.OpenGL; else if (bd.UseVulkan) flags |= SDL.WindowFlags.Vulkan; flags |= SDL.GetWindowFlags(bd.Window) & SDL.WindowFlags.HighPixelDensity; if (viewport.Flags.HasFlag(ImGuiViewportFlags.NoDecoration)) flags |= SDL.WindowFlags.Borderless; else flags |= SDL.WindowFlags.Resizable; if (viewport.Flags.HasFlag(ImGuiViewportFlags.NoTaskBarIcon)) flags |= SDL.WindowFlags.Utility; if (viewport.Flags.HasFlag(ImGuiViewportFlags.TopMost)) flags |= SDL.WindowFlags.AlwaysOnTop; vd.Window = SDL.CreateWindow("No Title Yet", (int)viewport.Size.X, (int)viewport.Size.Y, flags); vd.WindowID = SDL.GetWindowID(vd.Window); #if !__APPLE__ // On Mac, SDL3 parenting appears to prevent viewport from appearing on another monitor if (vd.ParentWindow != nint.Zero) SDL.SetWindowParent(vd.Window, vd.ParentWindow); #endif SDL.SetWindowPosition(vd.Window, (int)viewport.Pos.X, (int)viewport.Pos.Y); vd.WindowOwned = true; if (useOpengl) { vd.GLContext = SDL.GLCreateContext(vd.Window); SDL.GLSetSwapInterval(0); } if (useOpengl && backupContext != nint.Zero) SDL.GLMakeCurrent(bd.Window, backupContext); SetupPlatformHandles(viewport, vd.Window); Program.Logger.Log($"New SDL Viewport: {vd.Window} {vd.WindowID} {viewport.Pos.X} {viewport.Pos.Y} {viewport.Size.X} {viewport.Size.Y}"); #if WINDOWS viewport.PlatformHandleRaw = SDL.GetPointerProperty( SDL.GetWindowProperties(vd.Window), SDL.Properties.WindowWin32HWNDPointer, nint.Zero ).ToPointer(); #endif } internal static readonly ViewportCallback DestroyWindowDelegate = DestroyWindow; private static void DestroyWindow(ImGuiViewportPtr viewport) { ViewPortData? vd = ImGuiUserData.Get(viewport.PlatformUserData); if (vd != null) { if (vd.GLContext != nint.Zero && vd.WindowOwned) SDL.GLDestroyContext(vd.GLContext); if (vd.Window != nint.Zero && vd.WindowOwned) SDL.DestroyWindow(vd.Window); vd.GLContext = nint.Zero; vd.Window = nint.Zero; } ImGuiUserData.Free(viewport.PlatformUserData); viewport.PlatformUserData = null; viewport.PlatformHandle = null; viewport.PlatformHandleRaw = null; } internal static readonly ViewportCallback ShowWindowDelegate = ShowWindow; private static void ShowWindow(ImGuiViewportPtr viewport) { ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; #if __APPLE__ // On macOS new windows should always appear on top so they aren't hidden under existing ones. SDL.SetHint(SDL.Hints.WindowActivateWhenShown, "1"); #else SDL.SetHint(SDL.Hints.WindowActivateWhenShown, (viewport.Flags & ImGuiViewportFlags.NoFocusOnAppearing) != 0 ? "0" : "1"); #endif SDL.ShowWindow(vd.Window); } internal static readonly ViewportCallback UpdateWindowDelegate = UpdateWindow; private static void UpdateWindow(ImGuiViewportPtr viewport) { // C++ implementation is a no-op stub; left empty intentionally. } internal static readonly ViewportCallback RenderWindowDelegate = RenderWindow; private static void RenderWindow(ImGuiViewportPtr viewport) { ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; if (vd.GLContext != nint.Zero) SDL.GLMakeCurrent(vd.Window, vd.GLContext); } internal static readonly ViewportCallback SwapBuffersDelegate = SwapBuffers; private static void SwapBuffers(ImGuiViewportPtr viewport) { ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; if (vd.GLContext != nint.Zero) { SDL.GLMakeCurrent(vd.Window, vd.GLContext); SDL.GLSwapWindow(vd.Window); } } internal static readonly ViewportCallbackSetVec2 SetWindowPosDelegate = SetWindowPos; private static void SetWindowPos(ImGuiViewportPtr viewport, Vector2 pos) { ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; SDL.SetWindowPosition(vd.Window, (int)pos.X, (int)pos.Y); } internal static readonly ViewportCallbackGetVec2 GetWindowPosDelegate = GetWindowPos; private static Vector2 GetWindowPos(ImGuiViewportPtr viewport) { ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; SDL.GetWindowPosition(vd.Window, out int x, out int y); return new Vector2(x, y); } internal static readonly ViewportCallbackSetVec2 SetWindowSizeDelegate = SetWindowSize; private static void SetWindowSize(ImGuiViewportPtr viewport, Vector2 size) { ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; SDL.SetWindowSize(vd.Window, (int)size.X, (int)size.Y); } internal static readonly ViewportCallbackGetVec2 GetWindowSizeDelegate = GetWindowSize; private static Vector2 GetWindowSize(ImGuiViewportPtr viewport) { ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; SDL.GetWindowSize(vd.Window, out int w, out int h); return new Vector2(w, h); } internal static readonly ViewportCallbackGetVec2 GetWindowFramebufferScaleDelegate = GetWindowFramebufferScale; private static Vector2 GetWindowFramebufferScale(ImGuiViewportPtr viewport) { ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; GetWindowSizeAndFramebufferScale(vd.Window, out _, out Vector2 framebufferScale); return framebufferScale; } internal static readonly ViewportCallback SetWindowFocusDelegate = SetWindowFocus; private static void SetWindowFocus(ImGuiViewportPtr viewport) { ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; SDL.RaiseWindow(vd.Window); } internal static readonly ViewportCallbackBool GetWindowFocusDelegate = GetWindowFocus; private static bool GetWindowFocus(ImGuiViewportPtr viewport) { ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; return (SDL.GetWindowFlags(vd.Window) & SDL.WindowFlags.InputFocus) != 0; } internal static readonly ViewportCallbackBool GetWindowMinimizedDelegate = GetWindowMinimized; private static bool GetWindowMinimized(ImGuiViewportPtr viewport) { ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; return (SDL.GetWindowFlags(vd.Window) & SDL.WindowFlags.Minimized) != 0; } internal static readonly ViewportCallbackSetString SetWindowTitleDelegate = SetWindowTitle; private static void SetWindowTitle(ImGuiViewportPtr viewport, string title) { ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; SDL.SetWindowTitle(vd.Window, title); } internal static readonly ViewportCallbackSetFloat SetWindowAlphaDelegate = SetWindowAlpha; private static void SetWindowAlpha(ImGuiViewportPtr viewport, float alpha) { ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; SDL.SetWindowOpacity(vd.Window, alpha); } } // @formatter:on — enable formatter after this line } public enum GamepadMode { AutoFirst, AutoAll, Manual } public enum MouseCaptureMode { Enabled, EnabledAfterDrag, Disabled }