From 77bb73dbce707088bcf22a4bf7c21bab53e96506 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 28 Aug 2017 20:29:07 +0100 Subject: [PATCH] Added Multi-PIE support: - Added ImGui Context Manager to create and manage ImGui Context Proxies. - Changed ImGui Context Proxy to dynamically create context and allow pairing with input state. - Changed ImGui Module Manager to create one widget per context. - Changed ImGui Widget to work in different input modes. - Changed ImGui Input State to allow partial reset (only mouse or keyboard). --- Source/ImGui/Private/ImGuiContextManager.cpp | 71 ++++++++++ Source/ImGui/Private/ImGuiContextManager.h | 57 ++++++++ Source/ImGui/Private/ImGuiContextProxy.cpp | 55 +++++++- Source/ImGui/Private/ImGuiContextProxy.h | 33 +++-- Source/ImGui/Private/ImGuiImplementation.cpp | 8 ++ Source/ImGui/Private/ImGuiImplementation.h | 9 ++ Source/ImGui/Private/ImGuiInputState.cpp | 21 ++- Source/ImGui/Private/ImGuiInputState.h | 10 +- Source/ImGui/Private/ImGuiModuleManager.cpp | 23 ++- Source/ImGui/Private/ImGuiModuleManager.h | 19 +-- Source/ImGui/Private/SImGuiWidget.cpp | 132 ++++++++++++++---- Source/ImGui/Private/SImGuiWidget.h | 28 ++++ .../Private/Utilities/WorldContextIndex.h | 72 ++++++++++ 13 files changed, 469 insertions(+), 69 deletions(-) create mode 100644 Source/ImGui/Private/ImGuiContextManager.cpp create mode 100644 Source/ImGui/Private/ImGuiContextManager.h create mode 100644 Source/ImGui/Private/ImGuiImplementation.h create mode 100644 Source/ImGui/Private/Utilities/WorldContextIndex.h diff --git a/Source/ImGui/Private/ImGuiContextManager.cpp b/Source/ImGui/Private/ImGuiContextManager.cpp new file mode 100644 index 0000000..0f9c6b6 --- /dev/null +++ b/Source/ImGui/Private/ImGuiContextManager.cpp @@ -0,0 +1,71 @@ +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiPrivatePCH.h" + +#include "ImGuiContextManager.h" + + +FImGuiContextProxy& FImGuiContextManager::GetWorldContextProxy(UWorld& World) +{ + const int32 Index = Utilities::GetWorldContextIndex(World); + checkf(Index != Utilities::INVALID_CONTEXT_INDEX, TEXT("Couldn't resolve context index for world %s: WorldType = %d"), + *World.GetName(), World.WorldType); + +#if WITH_EDITOR + // Make sure that PIE worlds don't try to use editor context. + checkf(!GEngine->IsEditor() || Index != Utilities::DEFAULT_CONTEXT_INDEX, TEXT("Index for world %s " + "was resolved to the default context index %d, which in editor is reserved for editor context. PIE worlds " + "should use values that start from 1. WorldType = %d, NetMode = %d"), *World.GetName(), + Utilities::DEFAULT_CONTEXT_INDEX, World.WorldType, World.GetNetMode()); +#endif // WITH_EDITOR + + FContextData& Data = FindOrAddContextData(Index); + + // Track worlds to make sure that different worlds don't try to use the same context in the same time. + if (!Data.World.IsValid()) + { + Data.World = &World; + } + else + { + checkf(Data.World == &World, TEXT("Two different worlds, %s and %s, resolved to the same world context index %d."), + *Data.World->GetName(), *World.GetName(), Index); + } + + return Data.ContextProxy; +} + +void FImGuiContextManager::Tick(float DeltaSeconds) +{ + FContextData* Data = Contexts.Find(1); + if (!Data || !Data->World.IsValid()) + { + return; + } + + for (auto& Entry : Contexts) + { + FImGuiContextProxy& ContextProxy = Entry.Value.ContextProxy; + + ContextProxy.SetAsCurrent(); + + // Tick context proxy to end the old frame and starts a new one. + ContextProxy.Tick(DeltaSeconds); + } +} + +FImGuiContextManager::FContextData& FImGuiContextManager::FindOrAddContextData(int32 Index) +{ + FContextData* Data = Contexts.Find(Index); + + if (!Data) + { + Data = &Contexts.Add(Index); + if (!Data->ContextProxy.OnDraw().IsBoundToObject(&ImGuiDemo)) + { + Data->ContextProxy.OnDraw().AddRaw(&ImGuiDemo, &FImGuiDemo::DrawControls); + } + } + + return *Data; +} diff --git a/Source/ImGui/Private/ImGuiContextManager.h b/Source/ImGui/Private/ImGuiContextManager.h new file mode 100644 index 0000000..1ee584a --- /dev/null +++ b/Source/ImGui/Private/ImGuiContextManager.h @@ -0,0 +1,57 @@ +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include "ImGuiContextProxy.h" +#include "ImGuiDemo.h" +#include "Utilities/WorldContextIndex.h" + + +// Manages ImGui context proxies. +class FImGuiContextManager +{ +public: + + FImGuiContextManager() = default; + + FImGuiContextManager(const FImGuiContextManager&) = delete; + FImGuiContextManager& operator=(const FImGuiContextManager&) = delete; + + FImGuiContextManager(FImGuiContextManager&&) = delete; + FImGuiContextManager& operator=(FImGuiContextManager&&) = delete; + + // Get or create default ImGui context proxy. In editor this is the editor context proxy and in standalone game + // context proxy for the only world and the same value as returned from GetWorldContextProxy. + // + // If proxy doesn't exist then it will be created and initialized. + FImGuiContextProxy& GetDefaultContextProxy() { return FindOrAddContextData(Utilities::DEFAULT_CONTEXT_INDEX).ContextProxy; } + + // Get or create ImGui context proxy for given world. + // + // If proxy doesn't yet exist then it will be created and initialized. If proxy already exists then associated + // world data will be updated. + FImGuiContextProxy& GetWorldContextProxy(UWorld& World); + + // Get context proxy by index, or null if context with that index doesn't exist. + FORCEINLINE FImGuiContextProxy* GetContextProxy(int32 ContextIndex) + { + FContextData* Data = Contexts.Find(ContextIndex); + return Data ? &(Data->ContextProxy) : nullptr; + } + + void Tick(float DeltaSeconds); + +private: + + struct FContextData + { + TWeakObjectPtr World; + FImGuiContextProxy ContextProxy; + }; + + FContextData& FindOrAddContextData(int32 Index); + + TMap Contexts; + + FImGuiDemo ImGuiDemo; +}; diff --git a/Source/ImGui/Private/ImGuiContextProxy.cpp b/Source/ImGui/Private/ImGuiContextProxy.cpp index 7d367ec..901d574 100644 --- a/Source/ImGui/Private/ImGuiContextProxy.cpp +++ b/Source/ImGui/Private/ImGuiContextProxy.cpp @@ -4,17 +4,26 @@ #include "ImGuiContextProxy.h" +#include "ImGuiImplementation.h" +#include "ImGuiInteroperability.h" + static constexpr float DEFAULT_CANVAS_WIDTH = 3840.f; static constexpr float DEFAULT_CANVAS_HEIGHT = 2160.f; FImGuiContextProxy::FImGuiContextProxy() { + // Create context. + Context = ImGui::CreateContext(); + + // Set this context in ImGui for initialization (any allocations will be tracked in this context). + SetAsCurrent(); + + // Start initialization. ImGuiIO& IO = ImGui::GetIO(); // Use pre-defined canvas size. - IO.DisplaySize.x = DEFAULT_CANVAS_WIDTH; - IO.DisplaySize.y = DEFAULT_CANVAS_HEIGHT; + IO.DisplaySize = { DEFAULT_CANVAS_WIDTH, DEFAULT_CANVAS_HEIGHT }; // Load texture atlas. unsigned char* Pixels; @@ -28,12 +37,44 @@ FImGuiContextProxy::FImGuiContextProxy() BeginFrame(); } -FImGuiContextProxy::~FImGuiContextProxy() +FImGuiContextProxy::FImGuiContextProxy(FImGuiContextProxy&& Other) + : Context(std::move(Other.Context)) + , DrawEvent(std::move(Other.DrawEvent)) + , InputState(std::move(Other.InputState)) + , DrawLists(std::move(Other.DrawLists)) { - ImGui::Shutdown(); + Other.Context = nullptr; } -void FImGuiContextProxy::Tick(float DeltaSeconds, const FImGuiInputState* InputState) +FImGuiContextProxy& FImGuiContextProxy::operator=(FImGuiContextProxy&& Other) +{ + Context = std::move(Other.Context); + Other.Context = nullptr; + DrawEvent = std::move(Other.DrawEvent); + InputState = std::move(Other.InputState); + DrawLists = std::move(Other.DrawLists); + return *this; +} + +FImGuiContextProxy::~FImGuiContextProxy() +{ + if (Context) + { + // Set this context in ImGui for de-initialization (any de-allocations will be tracked in this context). + SetAsCurrent(); + + // Shutdown to save data etc. + ImGui::Shutdown(); + + // Destroy the context. + ImGui::DestroyContext(Context); + + // Set default context in ImGui to keep global context pointer valid. + ImGui::SetCurrentContext(&GetDefaultContext()); + } +} + +void FImGuiContextProxy::Tick(float DeltaSeconds) { if (bIsFrameStarted) { @@ -49,10 +90,10 @@ void FImGuiContextProxy::Tick(float DeltaSeconds, const FImGuiInputState* InputS } // Begin a new frame and set the context back to a state in which it allows to draw controls. - BeginFrame(DeltaSeconds, InputState); + BeginFrame(DeltaSeconds); } -void FImGuiContextProxy::BeginFrame(float DeltaTime, const FImGuiInputState* InputState) +void FImGuiContextProxy::BeginFrame(float DeltaTime) { if (!bIsFrameStarted) { diff --git a/Source/ImGui/Private/ImGuiContextProxy.h b/Source/ImGui/Private/ImGuiContextProxy.h index 21035ce..19e4b17 100644 --- a/Source/ImGui/Private/ImGuiContextProxy.h +++ b/Source/ImGui/Private/ImGuiContextProxy.h @@ -10,8 +10,7 @@ class FImGuiInputState; // Represents a single ImGui context. All the context updates should be done through this proxy. During update it -// broadcasts draw events to allow listeners draw their controls. After update it stores produced draw data. -// TODO: Add dynamically created contexts, so we can have a better support for multi-PIE. +// broadcasts draw events to allow listeners draw their controls. After update it stores draw data. class FImGuiContextProxy { public: @@ -22,30 +21,42 @@ public: FImGuiContextProxy(const FImGuiContextProxy&) = delete; FImGuiContextProxy& operator=(const FImGuiContextProxy&) = delete; - FImGuiContextProxy(FImGuiContextProxy&&) = delete; - FImGuiContextProxy& operator=(FImGuiContextProxy&&) = delete; + FImGuiContextProxy(FImGuiContextProxy&& Other); + FImGuiContextProxy& operator=(FImGuiContextProxy&& Other); // Get draw data from the last frame. const TArray& GetDrawData() const { return DrawLists; } + // Get input state used by this context. + const FImGuiInputState* GetInputState() const { return InputState; } + + // Set input state to be used by this context. + void SetInputState(const FImGuiInputState* SourceInputState) { InputState = SourceInputState; } + + // Is this context the current ImGui context. + bool IsCurrentContext() const { return ImGui::GetCurrentContext() == Context; } + + // Set this context as current ImGui context. + void SetAsCurrent() { ImGui::SetCurrentContext(Context); } + // Delegate called right before ending the frame to allows listeners draw their controls. FSimpleMulticastDelegate& OnDraw() { return DrawEvent; } // Tick to advance context to the next frame. - // @param DeltaSeconds - Time delta in seconds (will be passed to ImGui) - // @param InputState - Input state for ImGui IO or null if there is no input for this context - void Tick(float DeltaSeconds, const FImGuiInputState* InputState = nullptr); + void Tick(float DeltaSeconds); private: - void BeginFrame(float DeltaTime = 1.f / 60.f, const FImGuiInputState* InputState = nullptr); + void BeginFrame(float DeltaTime = 1.f / 60.f); void EndFrame(); void UpdateDrawData(ImDrawData* DrawData); - TArray DrawLists; - - FSimpleMulticastDelegate DrawEvent; + ImGuiContext* Context = nullptr; bool bIsFrameStarted = false; + FSimpleMulticastDelegate DrawEvent; + const FImGuiInputState* InputState = nullptr; + + TArray DrawLists; }; diff --git a/Source/ImGui/Private/ImGuiImplementation.cpp b/Source/ImGui/Private/ImGuiImplementation.cpp index 3c3e60a..48cb660 100644 --- a/Source/ImGui/Private/ImGuiImplementation.cpp +++ b/Source/ImGui/Private/ImGuiImplementation.cpp @@ -20,3 +20,11 @@ #if PLATFORM_WINDOWS #include #endif // PLATFORM_WINDOWS + + +// This is exposing ImGui default context for the whole module. +// This is assuming that we don't define custom GImGui and therefore have GImDefaultContext defined in imgui.cpp. +ImGuiContext& GetDefaultContext() +{ + return GImDefaultContext; +} diff --git a/Source/ImGui/Private/ImGuiImplementation.h b/Source/ImGui/Private/ImGuiImplementation.h new file mode 100644 index 0000000..5959b3b --- /dev/null +++ b/Source/ImGui/Private/ImGuiImplementation.h @@ -0,0 +1,9 @@ +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include + + +// Get default context created by ImGui framework. +ImGuiContext& GetDefaultContext(); diff --git a/Source/ImGui/Private/ImGuiInputState.cpp b/Source/ImGui/Private/ImGuiInputState.cpp index 5961183..b15dd43 100644 --- a/Source/ImGui/Private/ImGuiInputState.cpp +++ b/Source/ImGui/Private/ImGuiInputState.cpp @@ -45,15 +45,24 @@ void FImGuiInputState::SetMouseDown(uint32 MouseIndex, bool bIsDown) } } -void FImGuiInputState::ResetState() +void FImGuiInputState::Reset(bool bKeyboard, bool bMouse) { - ClearCharacters(); - ClearKeys(); + if (bKeyboard) + { + ClearCharacters(); + ClearKeys(); + } - ClearMouseButtons(); - ClearMouseAnalogue(); + if (bMouse) + { + ClearMouseButtons(); + ClearMouseAnalogue(); + } - ClearModifierKeys(); + if (bKeyboard && bMouse) + { + ClearModifierKeys(); + } } void FImGuiInputState::ClearUpdateState() diff --git a/Source/ImGui/Private/ImGuiInputState.h b/Source/ImGui/Private/ImGuiInputState.h index 1312723..853de90 100644 --- a/Source/ImGui/Private/ImGuiInputState.h +++ b/Source/ImGui/Private/ImGuiInputState.h @@ -98,7 +98,13 @@ public: void SetAltDown(bool bIsDown) { bIsAltDown = bIsDown; } // Reset state and mark as dirty. - void ResetState(); + void ResetState() { Reset(true, true); } + + // Reset keyboard state and mark as dirty. + void ResetKeyboardState() { Reset(true, false); } + + // Reset mouse state and mark as dirty. + void ResetMouseState() { Reset(false, true); } // Clear part of the state that is meant to be updated in every frame like: accumulators, buffers and information // about dirty parts of keys or mouse buttons arrays. @@ -106,6 +112,8 @@ public: private: + void Reset(bool bKeyboard, bool bMouse); + void ClearCharacters(); void ClearKeys(); void ClearMouseButtons(); diff --git a/Source/ImGui/Private/ImGuiModuleManager.cpp b/Source/ImGui/Private/ImGuiModuleManager.cpp index e496946..65a1497 100644 --- a/Source/ImGui/Private/ImGuiModuleManager.cpp +++ b/Source/ImGui/Private/ImGuiModuleManager.cpp @@ -11,8 +11,8 @@ FImGuiModuleManager::FImGuiModuleManager() { - // Bind ImGui demo to proxy, so it can draw controls in its context. - ContextProxy.OnDraw().AddRaw(&ImGuiDemo, &FImGuiDemo::DrawControls); + // Make sure that default ImGui context is setup. + ContextManager.GetDefaultContextProxy(); // Typically we will use viewport created events to add widget to new game viewports. ViewportCreatedHandle = UGameViewportClient::OnViewportCreated().AddRaw(this, &FImGuiModuleManager::OnViewportCreated); @@ -117,8 +117,8 @@ void FImGuiModuleManager::Tick(float DeltaSeconds) { if (IsInUpdateThread()) { - // Update context proxy to advance to next frame. - ContextProxy.Tick(DeltaSeconds, ViewportWidget.IsValid() ? &ViewportWidget->GetInputState() : nullptr); + // Update context manager to advance all ImGui contexts to the next frame. + ContextManager.Tick(DeltaSeconds); // Inform that we finished updating ImGui, so other subsystems can react. PostImGuiUpdateEvent.Broadcast(); @@ -141,15 +141,24 @@ void FImGuiModuleManager::AddWidgetToViewport(UGameViewportClient* GameViewport) checkf(GameViewport, TEXT("Null game viewport.")); checkf(FSlateApplication::IsInitialized(), TEXT("Slate should be initialized before we can add widget to game viewports.")); + const int32 ContextIndex = Utilities::GetWorldContextIndex(GameViewport); + + // This makes sure that context for this world is created. + auto& Proxy = ContextManager.GetWorldContextProxy(*GameViewport->GetWorld()); + + // Get widget for this world. + auto& ViewportWidget = ViewportWidgets.FindOrAdd(ContextIndex); if (!ViewportWidget.IsValid()) { - SAssignNew(ViewportWidget, SImGuiWidget).ModuleManager(this); - checkf(ViewportWidget.IsValid(), TEXT("Failed to create SImGuiWidget.")); + SAssignNew(ViewportWidget, SImGuiWidget).ModuleManager(this).ContextIndex(ContextIndex); + check(ViewportWidget.IsValid()); } + // Bind widget's input to context for this world. + Proxy.SetInputState(&ViewportWidget->GetInputState()); + // High enough z-order guarantees that ImGui output is rendered on top of the game UI. constexpr int32 IMGUI_WIDGET_Z_ORDER = 10000; - GameViewport->AddViewportWidgetContent(SNew(SWeakWidget).PossiblyNullContent(ViewportWidget), IMGUI_WIDGET_Z_ORDER); } diff --git a/Source/ImGui/Private/ImGuiModuleManager.h b/Source/ImGui/Private/ImGuiModuleManager.h index fade0ea..92301d0 100644 --- a/Source/ImGui/Private/ImGuiModuleManager.h +++ b/Source/ImGui/Private/ImGuiModuleManager.h @@ -2,8 +2,7 @@ #pragma once -#include "ImGuiContextProxy.h" -#include "ImGuiDemo.h" +#include "ImGuiContextManager.h" #include "SImGuiWidget.h" #include "TextureManager.h" @@ -16,8 +15,8 @@ class FImGuiModuleManager public: - // Get ImGui context proxy. - FImGuiContextProxy& GetContextProxy() { return ContextProxy; } + // Get ImGui contexts manager. + FImGuiContextManager& GetContextManager() { return ContextManager; } // Get texture resources manager. FTextureManager& GetTextureManager() { return TextureManager; } @@ -56,18 +55,14 @@ private: // Event that we call after ImGui is updated. FSimpleMulticastDelegate PostImGuiUpdateEvent; - // Proxy controlling ImGui context. - FImGuiContextProxy ContextProxy; - - // ImWidget that draws ImGui demo. - FImGuiDemo ImGuiDemo; + // Manager for ImGui contexts. + FImGuiContextManager ContextManager; // Manager for textures resources. FTextureManager TextureManager; - // Slate widget that we attach to created game viewports (widget without per-viewport state can be attached to - // multiple viewports). - TSharedPtr ViewportWidget; + // Slate widgets that we attach to game viewports. + TMap> ViewportWidgets; FDelegateHandle TickDelegateHandle; FDelegateHandle ViewportCreatedHandle; diff --git a/Source/ImGui/Private/SImGuiWidget.cpp b/Source/ImGui/Private/SImGuiWidget.cpp index 9660fd4..476b648 100644 --- a/Source/ImGui/Private/SImGuiWidget.cpp +++ b/Source/ImGui/Private/SImGuiWidget.cpp @@ -4,6 +4,7 @@ #include "SImGuiWidget.h" +#include "ImGuiContextManager.h" #include "ImGuiContextProxy.h" #include "ImGuiInteroperability.h" #include "ImGuiModuleManager.h" @@ -16,6 +17,7 @@ void SImGuiWidget::Construct(const FArguments& InArgs) { checkf(InArgs._ModuleManager, TEXT("Null Module Manager argument")); ModuleManager = InArgs._ModuleManager; + ContextIndex = InArgs._ContextIndex; ModuleManager->OnPostImGuiUpdate().AddRaw(this, &SImGuiWidget::OnPostImGuiUpdate); } @@ -95,6 +97,48 @@ FReply SImGuiWidget::OnMouseMove(const FGeometry& MyGeometry, const FPointerEven return FReply::Handled(); } +FReply SImGuiWidget::OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& FocusEvent) +{ + Super::OnFocusReceived(MyGeometry, FocusEvent); + + // If widget has a keyboard focus we always maintain mouse input. Technically, if mouse is outside of the widget + // area it won't generate events but we freeze its state until it either comes back or input is completely lost. + UpdateInputMode(true, true); + + return FReply::Handled(); +} + +void SImGuiWidget::OnFocusLost(const FFocusEvent& FocusEvent) +{ + Super::OnFocusLost(FocusEvent); + + UpdateInputMode(false, IsHovered()); +} + +void SImGuiWidget::OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + Super::OnMouseEnter(MyGeometry, MouseEvent); + + // If mouse enters while input is active then we need to update mouse buttons because there is a chance that we + // missed some events. + if (InputMode != EInputMode::None) + { + for (const FKey& Button : { EKeys::LeftMouseButton, EKeys::MiddleMouseButton, EKeys::RightMouseButton, EKeys::ThumbMouseButton, EKeys::ThumbMouseButton2 }) + { + InputState.SetMouseDown(ImGuiInterops::GetMouseIndex(Button), MouseEvent.IsMouseButtonDown(Button)); + } + } + + UpdateInputMode(HasKeyboardFocus(), true); +} + +void SImGuiWidget::OnMouseLeave(const FPointerEvent& MouseEvent) +{ + Super::OnMouseLeave(MouseEvent); + + UpdateInputMode(HasKeyboardFocus(), false); +} + void SImGuiWidget::CopyModifierKeys(const FInputEvent& InputEvent) { InputState.SetControlDown(InputEvent.IsControlDown()); @@ -102,47 +146,85 @@ void SImGuiWidget::CopyModifierKeys(const FInputEvent& InputEvent) InputState.SetAltDown(InputEvent.IsAltDown()); } +void SImGuiWidget::CopyModifierKeys(const FPointerEvent& MouseEvent) +{ + if (InputMode == EInputMode::MouseOnly) + { + CopyModifierKeys(static_cast(MouseEvent)); + } +} + +void SImGuiWidget::UpdateInputMode(bool bNeedKeyboard, bool bNeedMouse) +{ + const EInputMode NewInputMode = + bNeedKeyboard ? EInputMode::MouseAndKeyboard : + bNeedMouse ? EInputMode::MouseOnly : + EInputMode::None; + + if (InputMode != NewInputMode) + { + // We need to reset input components if we are either fully shutting down or we are downgrading from full to + // mouse-only input mode. + if (NewInputMode == EInputMode::None) + { + InputState.ResetState(); + } + else if (InputMode == EInputMode::MouseAndKeyboard) + { + InputState.ResetKeyboardState(); + } + + InputMode = NewInputMode; + } +} + void SImGuiWidget::OnPostImGuiUpdate() { - InputState.ClearUpdateState(); + if (InputMode != EInputMode::None) + { + InputState.ClearUpdateState(); + } } int32 SImGuiWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& WidgetStyle, bool bParentEnabled) const { - // Calculate offset that will transform vertex positions to screen space - rounded to avoid half pixel offsets. - const FVector2D VertexPositionOffset{ FMath::RoundToFloat(MyClippingRect.Left), FMath::RoundToFloat(MyClippingRect.Top) }; - - // Convert clipping rectangle to format required by Slate vertex. - const FSlateRotatedRect VertexClippingRect{ MyClippingRect }; - - for (const auto& DrawList : ModuleManager->GetContextProxy().GetDrawData()) + if (FImGuiContextProxy* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex)) { - DrawList.CopyVertexData(VertexBuffer, VertexPositionOffset, VertexClippingRect); + // Calculate offset that will transform vertex positions to screen space - rounded to avoid half pixel offsets. + const FVector2D VertexPositionOffset{ FMath::RoundToFloat(MyClippingRect.Left), FMath::RoundToFloat(MyClippingRect.Top) }; - // Get access to the Slate scissor rectangle defined in Slate Core API, so we can customize elements drawing. - extern SLATECORE_API TOptional GSlateScissorRect; + // Convert clipping rectangle to format required by Slate vertex. + const FSlateRotatedRect VertexClippingRect{ MyClippingRect }; - auto GSlateScissorRectSaver = ScopeGuards::MakeStateSaver(GSlateScissorRect); - - int IndexBufferOffset = 0; - for (int CommandNb = 0; CommandNb < DrawList.NumCommands(); CommandNb++) + for (const auto& DrawList : ContextProxy->GetDrawData()) { - const auto& DrawCommand = DrawList.GetCommand(CommandNb); + DrawList.CopyVertexData(VertexBuffer, VertexPositionOffset, VertexClippingRect); - DrawList.CopyIndexData(IndexBuffer, IndexBufferOffset, DrawCommand.NumElements); + // Get access to the Slate scissor rectangle defined in Slate Core API, so we can customize elements drawing. + extern SLATECORE_API TOptional GSlateScissorRect; - // Advance offset by number of copied elements to position it for the next command. - IndexBufferOffset += DrawCommand.NumElements; + auto GSlateScissorRectSaver = ScopeGuards::MakeStateSaver(GSlateScissorRect); - // Get texture resource handle for this draw command (null index will be also mapped to a valid texture). - const FSlateResourceHandle& Handle = ModuleManager->GetTextureManager().GetTextureHandle(DrawCommand.TextureId); + int IndexBufferOffset = 0; + for (int CommandNb = 0; CommandNb < DrawList.NumCommands(); CommandNb++) + { + const auto& DrawCommand = DrawList.GetCommand(CommandNb); - // Transform clipping rectangle to screen space and set in Slate, to apply it to elements that we draw. - GSlateScissorRect = FShortRect{ DrawCommand.ClippingRect.OffsetBy(MyClippingRect.GetTopLeft()).IntersectionWith(MyClippingRect) }; + DrawList.CopyIndexData(IndexBuffer, IndexBufferOffset, DrawCommand.NumElements); - // Add elements to the list. - FSlateDrawElement::MakeCustomVerts(OutDrawElements, LayerId, Handle, VertexBuffer, IndexBuffer, nullptr, 0, 0); + // Advance offset by number of copied elements to position it for the next command. + IndexBufferOffset += DrawCommand.NumElements; + + // Get texture resource handle for this draw command (null index will be also mapped to a valid texture). + const FSlateResourceHandle& Handle = ModuleManager->GetTextureManager().GetTextureHandle(DrawCommand.TextureId); + + // Transform clipping rectangle to screen space and set in Slate, to apply it to elements that we draw. + GSlateScissorRect = FShortRect{ DrawCommand.ClippingRect.OffsetBy(MyClippingRect.GetTopLeft()).IntersectionWith(MyClippingRect) }; + + // Add elements to the list. + FSlateDrawElement::MakeCustomVerts(OutDrawElements, LayerId, Handle, VertexBuffer, IndexBuffer, nullptr, 0, 0); + } } } diff --git a/Source/ImGui/Private/SImGuiWidget.h b/Source/ImGui/Private/SImGuiWidget.h index 56703b0..27bb843 100644 --- a/Source/ImGui/Private/SImGuiWidget.h +++ b/Source/ImGui/Private/SImGuiWidget.h @@ -19,12 +19,17 @@ public: SLATE_BEGIN_ARGS(SImGuiWidget) {} SLATE_ARGUMENT(FImGuiModuleManager*, ModuleManager) + SLATE_ARGUMENT(int32, ContextIndex) SLATE_END_ARGS() void Construct(const FArguments& InArgs); ~SImGuiWidget(); + // Get index of the context that this widget is targeting. + int32 GetContextIndex() const { return ContextIndex; } + + // Get input state associated with this widget. const FImGuiInputState& GetInputState() const { return InputState; } //---------------------------------------------------------------------------------------------------- @@ -49,9 +54,28 @@ public: virtual FReply OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + virtual FReply OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& FocusEvent) override; + + virtual void OnFocusLost(const FFocusEvent& FocusEvent) override; + + virtual void OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + + virtual void OnMouseLeave(const FPointerEvent& MouseEvent) override; + private: + enum class EInputMode + { + None, + MouseOnly, + MouseAndKeyboard + }; + FORCEINLINE void CopyModifierKeys(const FInputEvent& InputEvent); + FORCEINLINE void CopyModifierKeys(const FPointerEvent& MouseEvent); + + // Determine new input mode based on requirement hints. + void UpdateInputMode(bool bNeedKeyboard, bool bNeedMouse); void OnPostImGuiUpdate(); @@ -64,5 +88,9 @@ private: mutable TArray VertexBuffer; mutable TArray IndexBuffer; + int32 ContextIndex = 0; + + EInputMode InputMode = EInputMode::None; + FImGuiInputState InputState; }; diff --git a/Source/ImGui/Private/Utilities/WorldContextIndex.h b/Source/ImGui/Private/Utilities/WorldContextIndex.h new file mode 100644 index 0000000..4c42a8d --- /dev/null +++ b/Source/ImGui/Private/Utilities/WorldContextIndex.h @@ -0,0 +1,72 @@ +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include +#include + + +// Utilities mapping worlds to indices that we use to identify ImGui contexts. +// Editor and standalone games have context index 0 while PIE worlds have indices starting from 1 for server and 2+ for +// clients. + +namespace Utilities +{ + // Default context index for non PIE worlds. + static constexpr int32 DEFAULT_CONTEXT_INDEX = 0; + + // Invalid context index for parameters that cannot be resolved to a valid world. + static constexpr int32 INVALID_CONTEXT_INDEX = -1; + +#if WITH_EDITOR + + template + FORCEINLINE int32 GetWorldContextIndex(const TWeakObjectPtr& Obj) + { + return Obj.IsValid() ? GetWorldContextIndex(*Obj.Get()) : INVALID_CONTEXT_INDEX; + } + + template + FORCEINLINE int32 GetWorldContextIndex(const T* Obj) + { + return Obj ? GetWorldContextIndex(*Obj) : INVALID_CONTEXT_INDEX; + } + + FORCEINLINE int32 GetWorldContextIndex(const FWorldContext& WorldContext) + { + // In standalone game (WorldType = Game) we have only one context with index 0 (see DEFAULT_CONTEXT_INDEX). + + // In editor, we keep 0 for editor and use PIEInstance to index worlds. In simulation or standalone single-PIE + // sessions PIEInstance is 0, but since there is only one world we can change it without causing any conflicts. + // In single-PIE with dedicated server or multi-PIE sessions worlds have PIEInstance starting from 1 for server + // and 2+ for clients, what maps directly to our index. + + return WorldContext.WorldType == EWorldType::PIE ? FMath::Max(WorldContext.PIEInstance, 1) : DEFAULT_CONTEXT_INDEX; + } + + FORCEINLINE int32 GetWorldContextIndex(const UGameInstance& GameInstance) + { + return GetWorldContextIndex(GameInstance.GetWorldContext()); + } + + FORCEINLINE int32 GetWorldContextIndex(const UGameViewportClient& GameViewportClient) + { + return GetWorldContextIndex(GameViewportClient.GetGameInstance()); + } + + FORCEINLINE int32 GetWorldContextIndex(const UWorld& World) + { + return GetWorldContextIndex(World.GetGameInstance()); + } + +#else + + template + constexpr int32 GetWorldContextIndex(const T&) + { + // The only option is standalone game with one context. + return DEFAULT_CONTEXT_INDEX; + } + +#endif // #if WITH_EDITOR +}