// Distributed under the MIT License (MIT) (see accompanying LICENSE file) #include "SImGuiWidget.h" #include "SImGuiCanvasControl.h" #include "ImGuiContextManager.h" #include "ImGuiContextProxy.h" #include "ImGuiInputHandler.h" #include "ImGuiInputHandlerFactory.h" #include "ImGuiInteroperability.h" #include "ImGuiModuleManager.h" #include "ImGuiModuleSettings.h" #include "TextureManager.h" #include "Utilities/Arrays.h" #include "Utilities/ScopeGuards.h" #include "VersionCompatibility.h" #include #include #include #include #include #include #include #include #if IMGUI_WIDGET_DEBUG DEFINE_LOG_CATEGORY_STATIC(LogImGuiWidget, Warning, All); #define IMGUI_WIDGET_LOG(Verbosity, Format, ...) UE_LOG(LogImGuiWidget, Verbosity, Format, __VA_ARGS__) #define TEXT_INPUT_MODE(Val) (\ (Val) == EInputMode::Full ? TEXT("Full") :\ (Val) == EInputMode::MousePointerOnly ? TEXT("MousePointerOnly") :\ TEXT("None")) #define TEXT_BOOL(Val) ((Val) ? TEXT("true") : TEXT("false")) #else #define IMGUI_WIDGET_LOG(...) #endif // IMGUI_WIDGET_DEBUG #if IMGUI_WIDGET_DEBUG namespace CVars { TAutoConsoleVariable DebugWidget(TEXT("ImGui.Debug.Widget"), 0, TEXT("Show debug for SImGuiWidget.\n") TEXT("0: disabled (default)\n") TEXT("1: enabled"), ECVF_Default); TAutoConsoleVariable DebugInput(TEXT("ImGui.Debug.Input"), 0, TEXT("Show debug for input state.\n") TEXT("0: disabled (default)\n") TEXT("1: enabled"), ECVF_Default); } #endif // IMGUI_WIDGET_DEBUG namespace { FORCEINLINE FVector2D MaxVector(const FVector2D& A, const FVector2D& B) { return FVector2D(FMath::Max(A.X, B.X), FMath::Max(A.Y, B.Y)); } FORCEINLINE FVector2D RoundVector(const FVector2D& Vector) { return FVector2D(FMath::RoundToFloat(Vector.X), FMath::RoundToFloat(Vector.Y)); } FORCEINLINE FSlateRenderTransform RoundTranslation(const FSlateRenderTransform& Transform) { return FSlateRenderTransform(Transform.GetMatrix(), RoundVector(Transform.GetTranslation())); } } BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void SImGuiWidget::Construct(const FArguments& InArgs) { checkf(InArgs._ModuleManager, TEXT("Null Module Manager argument")); checkf(InArgs._GameViewport, TEXT("Null Game Viewport argument")); ModuleManager = InArgs._ModuleManager; GameViewport = InArgs._GameViewport; ContextIndex = InArgs._ContextIndex; // Register to get post-update notifications. ModuleManager->OnPostImGuiUpdate().AddRaw(this, &SImGuiWidget::OnPostImGuiUpdate); // Register debug delegate. auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex); checkf(ContextProxy, TEXT("Missing context during widget construction: ContextIndex = %d"), ContextIndex); #if IMGUI_WIDGET_DEBUG ContextProxy->OnDraw().AddRaw(this, &SImGuiWidget::OnDebugDraw); #endif // IMGUI_WIDGET_DEBUG // Register for settings change. RegisterImGuiSettingsDelegates(); // Get initial settings. const auto& Settings = ModuleManager->GetSettings(); SetHideMouseCursor(Settings.UseSoftwareCursor()); CreateInputHandler(Settings.GetImGuiInputHandlerClass()); SetDPIScale(Settings.GetDPIScaleInfo()); SetCanvasSizeInfo(Settings.GetCanvasSizeInfo()); // Initialize state. UpdateVisibility(); UpdateMouseCursor(); ChildSlot [ SAssignNew(CanvasControlWidget, SImGuiCanvasControl).OnTransformChanged(this, &SImGuiWidget::SetImGuiTransform) ]; ImGuiTransform = CanvasControlWidget->GetTransform(); } END_SLATE_FUNCTION_BUILD_OPTIMIZATION SImGuiWidget::~SImGuiWidget() { // Stop listening for settings change. UnregisterImGuiSettingsDelegates(); // Release ImGui Input Handler. ReleaseInputHandler(); // Remove binding between this widget and its context proxy. if (auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex)) { #if IMGUI_WIDGET_DEBUG ContextProxy->OnDraw().RemoveAll(this); #endif // IMGUI_WIDGET_DEBUG } // Unregister from post-update notifications. ModuleManager->OnPostImGuiUpdate().RemoveAll(this); } void SImGuiWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { Super::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); UpdateInputState(); UpdateTransparentMouseInput(AllottedGeometry); HandleWindowFocusLost(); UpdateCanvasSize(); } FReply SImGuiWidget::OnKeyChar(const FGeometry& MyGeometry, const FCharacterEvent& CharacterEvent) { return InputHandler->OnKeyChar(CharacterEvent); } FReply SImGuiWidget::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& KeyEvent) { UpdateCanvasControlMode(KeyEvent); return InputHandler->OnKeyDown(KeyEvent); } FReply SImGuiWidget::OnKeyUp(const FGeometry& MyGeometry, const FKeyEvent& KeyEvent) { UpdateCanvasControlMode(KeyEvent); return InputHandler->OnKeyUp(KeyEvent); } FReply SImGuiWidget::OnAnalogValueChanged(const FGeometry& MyGeometry, const FAnalogInputEvent& AnalogInputEvent) { return InputHandler->OnAnalogValueChanged(AnalogInputEvent); } FReply SImGuiWidget::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { return InputHandler->OnMouseButtonDown(MouseEvent).LockMouseToWidget(SharedThis(this)); } FReply SImGuiWidget::OnMouseButtonDoubleClick(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { return InputHandler->OnMouseButtonDoubleClick(MouseEvent).LockMouseToWidget(SharedThis(this)); } namespace { bool NeedMouseLock(const FPointerEvent& MouseEvent) { #if FROM_ENGINE_VERSION(4, 20) return FSlateApplication::Get().GetPressedMouseButtons().Num() > 0; #else return MouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton) || MouseEvent.IsMouseButtonDown(EKeys::MiddleMouseButton) || MouseEvent.IsMouseButtonDown(EKeys::RightMouseButton); #endif } } FReply SImGuiWidget::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { FReply Reply = InputHandler->OnMouseButtonUp(MouseEvent); if (!NeedMouseLock(MouseEvent)) { Reply.ReleaseMouseLock(); } return Reply; } FReply SImGuiWidget::OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { return InputHandler->OnMouseWheel(MouseEvent); } FReply SImGuiWidget::OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { return InputHandler->OnMouseMove(TransformScreenPointToImGui(MyGeometry, MouseEvent.GetScreenSpacePosition()), MouseEvent); } FReply SImGuiWidget::OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& FocusEvent) { Super::OnFocusReceived(MyGeometry, FocusEvent); IMGUI_WIDGET_LOG(VeryVerbose, TEXT("ImGui Widget %d - Focus Received."), ContextIndex); bForegroundWindow = GameViewport->Viewport->IsForegroundWindow(); InputHandler->OnKeyboardInputEnabled(); InputHandler->OnGamepadInputEnabled(); FSlateApplication::Get().ResetToDefaultPointerInputSettings(); return FReply::Handled(); } void SImGuiWidget::OnFocusLost(const FFocusEvent& FocusEvent) { Super::OnFocusLost(FocusEvent); IMGUI_WIDGET_LOG(VeryVerbose, TEXT("ImGui Widget %d - Focus Lost."), ContextIndex); InputHandler->OnKeyboardInputDisabled(); InputHandler->OnGamepadInputDisabled(); } void SImGuiWidget::OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { Super::OnMouseEnter(MyGeometry, MouseEvent); IMGUI_WIDGET_LOG(VeryVerbose, TEXT("ImGui Widget %d - Mouse Enter."), ContextIndex); InputHandler->OnMouseInputEnabled(); } void SImGuiWidget::OnMouseLeave(const FPointerEvent& MouseEvent) { Super::OnMouseLeave(MouseEvent); IMGUI_WIDGET_LOG(VeryVerbose, TEXT("ImGui Widget %d - Mouse Leave."), ContextIndex); InputHandler->OnMouseInputDisabled(); } FReply SImGuiWidget::OnTouchStarted(const FGeometry& MyGeometry, const FPointerEvent& TouchEvent) { return InputHandler->OnTouchStarted(TransformScreenPointToImGui(MyGeometry, TouchEvent.GetScreenSpacePosition()), TouchEvent); } FReply SImGuiWidget::OnTouchMoved(const FGeometry& MyGeometry, const FPointerEvent& TouchEvent) { return InputHandler->OnTouchMoved(TransformScreenPointToImGui(MyGeometry, TouchEvent.GetScreenSpacePosition()), TouchEvent); } FReply SImGuiWidget::OnTouchEnded(const FGeometry& MyGeometry, const FPointerEvent& TouchEvent) { UpdateVisibility(); return InputHandler->OnTouchEnded(TransformScreenPointToImGui(MyGeometry, TouchEvent.GetScreenSpacePosition()), TouchEvent); } void SImGuiWidget::CreateInputHandler(const FStringClassReference& HandlerClassReference) { ReleaseInputHandler(); if (!InputHandler.IsValid()) { InputHandler = FImGuiInputHandlerFactory::NewHandler(HandlerClassReference, ModuleManager, GameViewport.Get(), ContextIndex); } } void SImGuiWidget::ReleaseInputHandler() { if (InputHandler.IsValid()) { FImGuiInputHandlerFactory::ReleaseHandler(InputHandler.Get()); InputHandler.Reset(); } } void SImGuiWidget::RegisterImGuiSettingsDelegates() { auto& Settings = ModuleManager->GetSettings(); if (!Settings.OnImGuiInputHandlerClassChanged.IsBoundToObject(this)) { Settings.OnImGuiInputHandlerClassChanged.AddRaw(this, &SImGuiWidget::CreateInputHandler); } if (!Settings.OnUseSoftwareCursorChanged.IsBoundToObject(this)) { Settings.OnUseSoftwareCursorChanged.AddRaw(this, &SImGuiWidget::SetHideMouseCursor); } if (!Settings.OnDPIScaleChangedDelegate.IsBoundToObject(this)) { Settings.OnDPIScaleChangedDelegate.AddRaw(this, &SImGuiWidget::SetDPIScale); } if (!Settings.OnCanvasSizeChangedDelegate.IsBoundToObject(this)) { Settings.OnCanvasSizeChangedDelegate.AddRaw(this, &SImGuiWidget::SetCanvasSizeInfo); } } void SImGuiWidget::UnregisterImGuiSettingsDelegates() { auto& Settings = ModuleManager->GetSettings(); Settings.OnImGuiInputHandlerClassChanged.RemoveAll(this); Settings.OnUseSoftwareCursorChanged.RemoveAll(this); Settings.OnDPIScaleChangedDelegate.RemoveAll(this); Settings.OnCanvasSizeChangedDelegate.RemoveAll(this); } void SImGuiWidget::SetHideMouseCursor(bool bHide) { if (bHideMouseCursor != bHide) { bHideMouseCursor = bHide; UpdateMouseCursor(); } } bool SImGuiWidget::IsConsoleOpened() const { return GameViewport->ViewportConsole && GameViewport->ViewportConsole->ConsoleState != NAME_None; } void SImGuiWidget::UpdateVisibility() { // Make sure that we do not occlude other widgets, if input is disabled or if mouse is set to work in a transparent // mode (hit-test invisible). SetVisibility(bInputEnabled && !bTransparentMouseInput ? EVisibility::Visible : EVisibility::HitTestInvisible); IMGUI_WIDGET_LOG(VeryVerbose, TEXT("ImGui Widget %d - Visibility updated to '%s'."), ContextIndex, *GetVisibility().ToString()); } void SImGuiWidget::UpdateMouseCursor() { if (!bHideMouseCursor) { const FImGuiContextProxy* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex); SetCursor(ContextProxy ? ContextProxy->GetMouseCursor() : EMouseCursor::Default); } else { SetCursor(EMouseCursor::None); } } ULocalPlayer* SImGuiWidget::GetLocalPlayer() const { if (GameViewport.IsValid()) { if (UWorld* World = GameViewport->GetWorld()) { if (ULocalPlayer* LocalPlayer = World->GetFirstLocalPlayerFromController()) { return World->GetFirstLocalPlayerFromController(); } } } return nullptr; } void SImGuiWidget::TakeFocus() { auto& SlateApplication = FSlateApplication::Get(); PreviousUserFocusedWidget = SlateApplication.GetUserFocusedWidget(SlateApplication.GetUserIndexForKeyboard()); if (ULocalPlayer* LocalPlayer = GetLocalPlayer()) { TSharedRef FocusWidget = SharedThis(this); LocalPlayer->GetSlateOperations().CaptureMouse(FocusWidget); LocalPlayer->GetSlateOperations().SetUserFocus(FocusWidget); } else { SlateApplication.SetKeyboardFocus(SharedThis(this)); } } void SImGuiWidget::ReturnFocus() { if (HasKeyboardFocus()) { auto FocusWidgetPtr = PreviousUserFocusedWidget.IsValid() ? PreviousUserFocusedWidget.Pin() : GameViewport->GetGameViewportWidget(); if (ULocalPlayer* LocalPlayer = GetLocalPlayer()) { auto FocusWidgetRef = FocusWidgetPtr.ToSharedRef(); LocalPlayer->GetSlateOperations().CaptureMouse(FocusWidgetRef); LocalPlayer->GetSlateOperations().SetUserFocus(FocusWidgetRef); } else { auto& SlateApplication = FSlateApplication::Get(); SlateApplication.ResetToDefaultPointerInputSettings(); SlateApplication.SetUserFocus(SlateApplication.GetUserIndexForKeyboard(), FocusWidgetPtr); } } PreviousUserFocusedWidget.Reset(); } void SImGuiWidget::UpdateInputState() { auto& Properties = ModuleManager->GetProperties(); auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex); const bool bEnableTransparentMouseInput = Properties.IsMouseInputShared() #if PLATFORM_ANDROID || PLATFORM_IOS && (FSlateApplication::Get().GetCursorPos() != FVector2D::ZeroVector) #endif && !(ContextProxy->WantsMouseCapture() || ContextProxy->HasActiveItem()); if (bTransparentMouseInput != bEnableTransparentMouseInput) { bTransparentMouseInput = bEnableTransparentMouseInput; if (bInputEnabled) { UpdateVisibility(); } } const bool bEnableInput = Properties.IsInputEnabled(); if (bInputEnabled != bEnableInput) { IMGUI_WIDGET_LOG(Log, TEXT("ImGui Widget %d - Input Enabled changed to '%s'."), ContextIndex, TEXT_BOOL(bEnableInput)); bInputEnabled = bEnableInput; UpdateVisibility(); UpdateMouseCursor(); if (bInputEnabled) { // We won't get mouse enter, if viewport is already hovered. if (GameViewport->GetGameViewportWidget()->IsHovered()) { InputHandler->OnMouseInputEnabled(); } TakeFocus(); } else { ReturnFocus(); } } else if(bInputEnabled) { const auto& ViewportWidget = GameViewport->GetGameViewportWidget(); if (bTransparentMouseInput) { // If mouse is in transparent input mode and focus is lost to viewport, let viewport keep it and disable // the whole input to match that state. if (GameViewport->GetGameViewportWidget()->HasMouseCapture()) { Properties.SetInputEnabled(false); UpdateInputState(); } } else { // Widget tends to lose keyboard focus after console is opened. With non-transparent mouse we can fix that // by manually restoring it. if (!HasKeyboardFocus() && !IsConsoleOpened() && (ViewportWidget->HasKeyboardFocus() || ViewportWidget->HasFocusedDescendants())) { TakeFocus(); } } } } void SImGuiWidget::UpdateTransparentMouseInput(const FGeometry& AllottedGeometry) { if (bInputEnabled && bTransparentMouseInput) { if (!GameViewport->GetGameViewportWidget()->HasMouseCapture()) { InputHandler->OnMouseMove(TransformScreenPointToImGui(AllottedGeometry, FSlateApplication::Get().GetCursorPos())); } } } void SImGuiWidget::HandleWindowFocusLost() { // We can use window foreground status to notify about application losing or receiving focus. In some situations // we get mouse leave or enter events, but they are only sent if mouse pointer is inside of the viewport. if (bInputEnabled && HasKeyboardFocus()) { if (bForegroundWindow != GameViewport->Viewport->IsForegroundWindow()) { bForegroundWindow = !bForegroundWindow; IMGUI_WIDGET_LOG(VeryVerbose, TEXT("ImGui Widget %d - Updating input after %s foreground window status."), ContextIndex, bForegroundWindow ? TEXT("getting") : TEXT("losing")); if (bForegroundWindow) { InputHandler->OnKeyboardInputEnabled(); InputHandler->OnGamepadInputEnabled(); } else { InputHandler->OnKeyboardInputDisabled(); InputHandler->OnGamepadInputDisabled(); } } } } void SImGuiWidget::SetDPIScale(const FImGuiDPIScaleInfo& ScaleInfo) { const float Scale = ScaleInfo.GetSlateScale(); if (DPIScale != Scale) { DPIScale = Scale; bUpdateCanvasSize = true; } } void SImGuiWidget::SetCanvasSizeInfo(const FImGuiCanvasSizeInfo& CanvasSizeInfo) { switch (CanvasSizeInfo.SizeType) { case EImGuiCanvasSizeType::Custom: MinCanvasSize = { static_cast(CanvasSizeInfo.Width), static_cast(CanvasSizeInfo.Height) }; bAdaptiveCanvasSize = CanvasSizeInfo.bExtendToViewport; bCanvasControlEnabled = true; break; case EImGuiCanvasSizeType::Desktop: MinCanvasSize = (GEngine && GEngine->GameUserSettings) ? GEngine->GameUserSettings->GetDesktopResolution() : FVector2D::ZeroVector; bAdaptiveCanvasSize = CanvasSizeInfo.bExtendToViewport; bCanvasControlEnabled = true; break; case EImGuiCanvasSizeType::Viewport: default: MinCanvasSize = FVector2D::ZeroVector; bAdaptiveCanvasSize = true; bCanvasControlEnabled = false; } // We only update canvas control widget when canvas control is enabled. Make sure that we will not leave // that widget active after disabling canvas control. if (CanvasControlWidget.IsValid() && !bCanvasControlEnabled) { CanvasControlWidget->SetActive(false); } bUpdateCanvasSize = true; } void SImGuiWidget::UpdateCanvasSize() { if (bUpdateCanvasSize) { if (auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex)) { CanvasSize = MinCanvasSize; if (bAdaptiveCanvasSize && GameViewport.IsValid()) { FVector2D ViewportSize; GameViewport->GetViewportSize(ViewportSize); CanvasSize = MaxVector(CanvasSize, ViewportSize); } else { // No need for more updates, if we successfully processed fixed-canvas size. bUpdateCanvasSize = false; } // Clamping DPI Scale to keep the canvas size from getting too big. CanvasSize /= FMath::Max(DPIScale, 0.01f); CanvasSize = RoundVector(CanvasSize); ContextProxy->SetDisplaySize(CanvasSize); } } } void SImGuiWidget::UpdateCanvasControlMode(const FInputEvent& InputEvent) { if (bCanvasControlEnabled) { CanvasControlWidget->SetActive(InputEvent.IsLeftAltDown() && InputEvent.IsLeftShiftDown()); } } void SImGuiWidget::OnPostImGuiUpdate() { ImGuiRenderTransform = ImGuiTransform; UpdateMouseCursor(); } FVector2D SImGuiWidget::TransformScreenPointToImGui(const FGeometry& MyGeometry, const FVector2D& Point) const { const FSlateRenderTransform ImGuiToScreen = ImGuiTransform.Concatenate(MyGeometry.GetAccumulatedRenderTransform()); return ImGuiToScreen.Inverse().TransformPoint(Point); } int32 SImGuiWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& WidgetStyle, bool bParentEnabled) const { if (FImGuiContextProxy* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex)) { // Manually update ImGui context to minimise lag between creating and rendering ImGui output. This will also // keep frame tearing at minimum because it is executed at the very end of the frame. ContextProxy->Tick(FSlateApplication::Get().GetDeltaTime()); // Calculate transform from ImGui to screen space. Rounding translation is necessary to keep it pixel-perfect // in older engine versions. const FSlateRenderTransform& WidgetToScreen = AllottedGeometry.GetAccumulatedRenderTransform(); const FSlateRenderTransform ImGuiToScreen = RoundTranslation(ImGuiRenderTransform.Concatenate(WidgetToScreen)); #if ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API // Convert clipping rectangle to format required by Slate vertex. const FSlateRotatedRect VertexClippingRect{ MyClippingRect }; #endif // ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API for (const auto& DrawList : ContextProxy->GetDrawData()) { #if ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API DrawList.CopyVertexData(VertexBuffer, ImGuiToScreen, VertexClippingRect); // Get access to the Slate scissor rectangle defined in Slate Core API, so we can customize elements drawing. extern SLATECORE_API TOptional GSlateScissorRect; auto GSlateScissorRectSaver = ScopeGuards::MakeStateSaver(GSlateScissorRect); #else DrawList.CopyVertexData(VertexBuffer, ImGuiToScreen); #endif // ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API int IndexBufferOffset = 0; for (int CommandNb = 0; CommandNb < DrawList.NumCommands(); CommandNb++) { const auto& DrawCommand = DrawList.GetCommand(CommandNb, ImGuiToScreen); DrawList.CopyIndexData(IndexBuffer, IndexBufferOffset, DrawCommand.NumElements); // 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 apply to elements that we draw. const FSlateRect ClippingRect = DrawCommand.ClippingRect.IntersectionWith(MyClippingRect); #if ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API GSlateScissorRect = FShortRect{ ClippingRect }; #else OutDrawElements.PushClip(FSlateClippingZone{ ClippingRect }); #endif // ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API // Add elements to the list. FSlateDrawElement::MakeCustomVerts(OutDrawElements, LayerId, Handle, VertexBuffer, IndexBuffer, nullptr, 0, 0); #if !ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API OutDrawElements.PopClip(); #endif // ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API } } } return Super::OnPaint(Args, AllottedGeometry, MyClippingRect, OutDrawElements, LayerId, WidgetStyle, bParentEnabled); } FVector2D SImGuiWidget::ComputeDesiredSize(float Scale) const { return CanvasSize * Scale; } #if IMGUI_WIDGET_DEBUG static TArray GetImGuiMappedKeys() { TArray Keys; Keys.Reserve(Utilities::ArraySize::value + 8); // ImGui IO key map. Keys.Emplace(EKeys::Tab); Keys.Emplace(EKeys::Left); Keys.Emplace(EKeys::Right); Keys.Emplace(EKeys::Up); Keys.Emplace(EKeys::Down); Keys.Emplace(EKeys::PageUp); Keys.Emplace(EKeys::PageDown); Keys.Emplace(EKeys::Home); Keys.Emplace(EKeys::End); Keys.Emplace(EKeys::Delete); Keys.Emplace(EKeys::BackSpace); Keys.Emplace(EKeys::Enter); Keys.Emplace(EKeys::Escape); Keys.Emplace(EKeys::A); Keys.Emplace(EKeys::C); Keys.Emplace(EKeys::V); Keys.Emplace(EKeys::X); Keys.Emplace(EKeys::Y); Keys.Emplace(EKeys::Z); // Modifier keys. Keys.Emplace(EKeys::LeftShift); Keys.Emplace(EKeys::RightShift); Keys.Emplace(EKeys::LeftControl); Keys.Emplace(EKeys::RightControl); Keys.Emplace(EKeys::LeftAlt); Keys.Emplace(EKeys::RightAlt); Keys.Emplace(EKeys::LeftCommand); Keys.Emplace(EKeys::RightCommand); return Keys; } // Column layout utilities. namespace Columns { template static void CollapsingGroup(const char* Name, int Columns, FunctorType&& DrawContent) { if (ImGui::CollapsingHeader(Name, ImGuiTreeNodeFlags_DefaultOpen)) { const int LastColumns = ImGui::GetColumnsCount(); ImGui::Columns(Columns, nullptr, false); DrawContent(); ImGui::Columns(LastColumns); } } } // Controls tweaked for 2-columns layout. namespace TwoColumns { template static inline void CollapsingGroup(const char* Name, FunctorType&& DrawContent) { Columns::CollapsingGroup(Name, 2, std::forward(DrawContent)); } namespace { void LabelText(const char* Label) { ImGui::Text("%s:", Label); } void LabelText(const wchar_t* Label) { ImGui::Text("%ls:", Label); } } template static void Value(LabelType&& Label, int32 Value) { LabelText(Label); ImGui::NextColumn(); ImGui::Text("%d", Value); ImGui::NextColumn(); } template static void Value(LabelType&& Label, uint32 Value) { LabelText(Label); ImGui::NextColumn(); ImGui::Text("%u", Value); ImGui::NextColumn(); } template static void Value(LabelType&& Label, float Value) { LabelText(Label); ImGui::NextColumn(); ImGui::Text("%f", Value); ImGui::NextColumn(); } template static void Value(LabelType&& Label, bool bValue) { LabelText(Label); ImGui::NextColumn(); ImGui::Text("%ls", TEXT_BOOL(bValue)); ImGui::NextColumn(); } template static void Value(LabelType&& Label, const TCHAR* Value) { LabelText(Label); ImGui::NextColumn(); ImGui::Text("%ls", Value); ImGui::NextColumn(); } template static void ValueWidthHeight(LabelType&& Label, const FVector2D& Value) { LabelText(Label); ImGui::NextColumn(); ImGui::Text("Width = %.0f, Height = %.0f", Value.X, Value.Y); ImGui::NextColumn(); } } namespace Styles { template static void TextHighlight(bool bHighlight, FunctorType&& DrawContent) { if (bHighlight) { ImGui::PushStyleColor(ImGuiCol_Text, { 1.f, 1.f, 0.5f, 1.f }); } DrawContent(); if (bHighlight) { ImGui::PopStyleColor(); } } } void SImGuiWidget::OnDebugDraw() { FImGuiContextProxy* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex); if (CVars::DebugWidget.GetValueOnGameThread() > 0) { bool bDebug = true; ImGui::SetNextWindowSize(ImVec2(380, 480), ImGuiCond_Once); if (ImGui::Begin("ImGui Widget Debug", &bDebug)) { ImGui::Spacing(); TwoColumns::CollapsingGroup("Canvas Size", [&]() { TwoColumns::Value("Is Adaptive", bAdaptiveCanvasSize); TwoColumns::Value("Is Updating", bUpdateCanvasSize); TwoColumns::ValueWidthHeight("Min Canvas Size", MinCanvasSize); TwoColumns::ValueWidthHeight("Canvas Size", CanvasSize); TwoColumns::Value("DPI Scale", DPIScale); }); TwoColumns::CollapsingGroup("Context", [&]() { TwoColumns::Value("Context Index", ContextIndex); TwoColumns::Value("Context Name", ContextProxy ? *ContextProxy->GetName() : TEXT("< Null >")); TwoColumns::Value("Game Viewport", *GameViewport->GetName()); }); TwoColumns::CollapsingGroup("Input Mode", [&]() { TwoColumns::Value("Input Enabled", bInputEnabled); }); TwoColumns::CollapsingGroup("Widget", [&]() { TwoColumns::Value("Visibility", *GetVisibility().ToString()); TwoColumns::Value("Is Hovered", IsHovered()); TwoColumns::Value("Is Directly Hovered", IsDirectlyHovered()); TwoColumns::Value("Has Keyboard Input", HasKeyboardFocus()); }); TwoColumns::CollapsingGroup("Viewport", [&]() { const auto& ViewportWidget = GameViewport->GetGameViewportWidget(); TwoColumns::Value("Is Foreground Window", GameViewport->Viewport->IsForegroundWindow()); TwoColumns::Value("Is Hovered", ViewportWidget->IsHovered()); TwoColumns::Value("Is Directly Hovered", ViewportWidget->IsDirectlyHovered()); TwoColumns::Value("Has Mouse Capture", ViewportWidget->HasMouseCapture()); TwoColumns::Value("Has Keyboard Input", ViewportWidget->HasKeyboardFocus()); TwoColumns::Value("Has Focused Descendants", ViewportWidget->HasFocusedDescendants()); auto Widget = PreviousUserFocusedWidget.Pin(); TwoColumns::Value("Previous User Focused", Widget.IsValid() ? *Widget->GetTypeAsString() : TEXT("None")); }); } ImGui::End(); if (!bDebug) { CVars::DebugWidget->Set(0, ECVF_SetByConsole); } } if (ContextProxy && CVars::DebugInput.GetValueOnGameThread() > 0) { FImGuiInputState& InputState = ContextProxy->GetInputState(); bool bDebug = true; ImGui::SetNextWindowSize(ImVec2(460, 480), ImGuiCond_Once); if (ImGui::Begin("ImGui Input State", &bDebug)) { const ImVec4 HiglightColor{ 1.f, 1.f, 0.5f, 1.f }; Columns::CollapsingGroup("Mapped Keys", 4, [&]() { static const auto& Keys = GetImGuiMappedKeys(); const int32 Num = Keys.Num(); // Simplified when slicing for two 2. const int32 RowsNum = (Num + 1) / 2; for (int32 Row = 0; Row < RowsNum; Row++) { for (int32 Col = 0; Col < 2; Col++) { const int32 Idx = Row + Col * RowsNum; if (Idx < Num) { const FKey& Key = Keys[Idx]; const uint32 KeyIndex = ImGuiInterops::GetKeyIndex(Key); Styles::TextHighlight(InputState.GetKeys()[KeyIndex], [&]() { TwoColumns::Value(*Key.GetDisplayName().ToString(), KeyIndex); }); } else { ImGui::NextColumn(); ImGui::NextColumn(); } } } }); Columns::CollapsingGroup("Modifier Keys", 4, [&]() { Styles::TextHighlight(InputState.IsShiftDown(), [&]() { ImGui::Text("Shift"); }); ImGui::NextColumn(); Styles::TextHighlight(InputState.IsControlDown(), [&]() { ImGui::Text("Control"); }); ImGui::NextColumn(); Styles::TextHighlight(InputState.IsAltDown(), [&]() { ImGui::Text("Alt"); }); ImGui::NextColumn(); ImGui::NextColumn(); }); Columns::CollapsingGroup("Mouse Buttons", 4, [&]() { static const FKey Buttons[] = { EKeys::LeftMouseButton, EKeys::RightMouseButton, EKeys::MiddleMouseButton, EKeys::ThumbMouseButton, EKeys::ThumbMouseButton2 }; const int32 Num = Utilities::GetArraySize(Buttons); // Simplified when slicing for two 2. const int32 RowsNum = (Num + 1) / 2; for (int32 Row = 0; Row < RowsNum; Row++) { for (int32 Col = 0; Col < 2; Col++) { const int32 Idx = Row + Col * RowsNum; if (Idx < Num) { const FKey& Button = Buttons[Idx]; const uint32 MouseIndex = ImGuiInterops::GetMouseIndex(Button); Styles::TextHighlight(InputState.GetMouseButtons()[MouseIndex], [&]() { TwoColumns::Value(*Button.GetDisplayName().ToString(), MouseIndex); }); } else { ImGui::NextColumn(); ImGui::NextColumn(); } } } }); Columns::CollapsingGroup("Mouse Axes", 4, [&]() { TwoColumns::Value("Position X", InputState.GetMousePosition().X); TwoColumns::Value("Position Y", InputState.GetMousePosition().Y); TwoColumns::Value("Wheel Delta", InputState.GetMouseWheelDelta()); ImGui::NextColumn(); ImGui::NextColumn(); }); if (!bDebug) { CVars::DebugInput->Set(0, ECVF_SetByConsole); } } ImGui::End(); } } #undef TEXT_INPUT_MODE #undef TEXT_BOOL #endif // IMGUI_WIDGET_DEBUG #undef IMGUI_WIDGET_LOG