mirror of
https://github.com/kevinporetti/UnrealImGui.git
synced 2025-01-18 16:30:32 +00:00
353 lines
11 KiB
C++
353 lines
11 KiB
C++
// Distributed under the MIT License (MIT) (see accompanying LICENSE file)
|
|
|
|
#include "SImGuiCanvasControl.h"
|
|
|
|
#include "VersionCompatibility.h"
|
|
|
|
#include <Rendering/DrawElements.h>
|
|
#include <SlateOptMacros.h>
|
|
|
|
|
|
namespace
|
|
{
|
|
// Mouse wheel to zoom ratio - how fast zoom changes with mouse wheel delta.
|
|
const float ZoomScrollSpeed = 0.03125f;
|
|
|
|
// Speed of blending out - how much zoom scale and canvas offset fades in every frame.
|
|
const float BlendOutSpeed = 0.25f;
|
|
|
|
// TODO: Move to settings
|
|
namespace Colors
|
|
{
|
|
const FLinearColor CanvasMargin = FColor(0, 4, 32, 64);
|
|
const FLinearColor CanvasBorder = FColor::Black.WithAlpha(127);
|
|
const FLinearColor CanvasBorderHighlight = FColor(16, 24, 64, 160);
|
|
const FLinearColor FrameBorder = FColor(222, 163, 9, 128);
|
|
const FLinearColor FrameBorderHighlight = FColor(255, 180, 10, 160);
|
|
}
|
|
|
|
// Defines type of drag operation.
|
|
enum class EDragType
|
|
{
|
|
Content,
|
|
Canvas
|
|
};
|
|
|
|
// Data for drag & drop operations. Calculations are made in widget where we have more straightforward access to data
|
|
// like geometry or scale.
|
|
class FImGuiDragDropOperation : public FDragDropOperation
|
|
{
|
|
public:
|
|
|
|
DRAG_DROP_OPERATOR_TYPE(FImGuiDragDropOperation, FDragDropOperation)
|
|
|
|
FImGuiDragDropOperation(const FVector2D& InPosition, const FVector2D& InOffset, EDragType InDragType)
|
|
: StartPosition(InPosition)
|
|
, StartOffset(InOffset)
|
|
, DragType(InDragType)
|
|
{
|
|
bCreateNewWindow = false;
|
|
MouseCursor = EMouseCursor::GrabHandClosed;
|
|
}
|
|
|
|
FVector2D StartPosition;
|
|
FVector2D StartOffset;
|
|
EDragType DragType;
|
|
};
|
|
}
|
|
|
|
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
|
|
void SImGuiCanvasControl::Construct(const FArguments& InArgs)
|
|
{
|
|
OnTransformChanged = InArgs._OnTransformChanged;
|
|
UpdateVisibility();
|
|
}
|
|
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
|
|
|
|
void SImGuiCanvasControl::SetActive(bool bInActive)
|
|
{
|
|
if (bActive != bInActive)
|
|
{
|
|
bActive = bInActive;
|
|
bBlendingOut = !bInActive;
|
|
if (bInActive)
|
|
{
|
|
Opacity = 1.f;
|
|
}
|
|
UpdateVisibility();
|
|
}
|
|
}
|
|
|
|
void SImGuiCanvasControl::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
|
|
{
|
|
Super::Tick(AllottedGeometry, InCurrentTime, InDeltaTime);
|
|
|
|
if (bBlendingOut)
|
|
{
|
|
if (FMath::IsNearlyEqual(CanvasScale, 1.f, ZoomScrollSpeed) && CanvasOffset.IsNearlyZero(1.f))
|
|
{
|
|
CanvasOffset = FVector2D::ZeroVector;
|
|
CanvasScale = 1.f;
|
|
Opacity = 0.f;
|
|
bBlendingOut = false;
|
|
UpdateVisibility();
|
|
}
|
|
else
|
|
{
|
|
CanvasOffset = FMath::Lerp(CanvasOffset, FVector2D::ZeroVector, BlendOutSpeed);
|
|
CanvasScale = FMath::Lerp(CanvasScale, 1.f, BlendOutSpeed);
|
|
Opacity = FMath::Lerp(Opacity, 0.f, BlendOutSpeed);
|
|
}
|
|
|
|
UpdateRenderTransform();
|
|
}
|
|
}
|
|
|
|
FReply SImGuiCanvasControl::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
|
|
{
|
|
if (DragRequest == EDragRequest::None)
|
|
{
|
|
if (MouseEvent.GetEffectingButton() == EKeys::RightMouseButton)
|
|
{
|
|
DragRequest = EDragRequest::Content;
|
|
return FReply::Handled().DetectDrag(SharedThis(this), EKeys::RightMouseButton).CaptureMouse(SharedThis(this));
|
|
}
|
|
else if (MouseEvent.GetEffectingButton() == EKeys::MiddleMouseButton)
|
|
{
|
|
DragRequest = EDragRequest::Canvas;
|
|
return FReply::Handled().DetectDrag(SharedThis(this), EKeys::MiddleMouseButton).CaptureMouse(SharedThis(this));
|
|
}
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
FReply SImGuiCanvasControl::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
|
|
{
|
|
if (MouseEvent.GetEffectingButton() == EKeys::RightMouseButton)
|
|
{
|
|
if (DragRequest == EDragRequest::Content)
|
|
{
|
|
DragRequest = EDragRequest::None;
|
|
}
|
|
}
|
|
else if (MouseEvent.GetEffectingButton() == EKeys::MiddleMouseButton)
|
|
{
|
|
if (DragRequest == EDragRequest::Canvas)
|
|
{
|
|
DragRequest = EDragRequest::None;
|
|
}
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
FReply SImGuiCanvasControl::OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
|
|
{
|
|
Zoom(MyGeometry, MouseEvent.GetWheelDelta() * ZoomScrollSpeed, MouseEvent.GetScreenSpacePosition());
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
FReply SImGuiCanvasControl::OnDragDetected(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
|
|
{
|
|
if (DragRequest == EDragRequest::Content)
|
|
{
|
|
return FReply::Handled()
|
|
.BeginDragDrop(MakeShareable(new FImGuiDragDropOperation(
|
|
MouseEvent.GetScreenSpacePosition(), ContentOffset, EDragType::Content)))
|
|
.LockMouseToWidget(SharedThis(this));
|
|
}
|
|
else if (DragRequest == EDragRequest::Canvas)
|
|
{
|
|
return FReply::Handled()
|
|
.BeginDragDrop(MakeShareable(new FImGuiDragDropOperation(
|
|
MouseEvent.GetScreenSpacePosition(), CanvasOffset, EDragType::Canvas)))
|
|
.LockMouseToWidget(SharedThis(this));
|
|
}
|
|
else
|
|
{
|
|
return FReply::Unhandled();
|
|
}
|
|
}
|
|
|
|
FReply SImGuiCanvasControl::OnDragOver(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent)
|
|
{
|
|
auto Operation = DragDropEvent.GetOperationAs<FImGuiDragDropOperation>();
|
|
if (Operation.IsValid())
|
|
{
|
|
const FSlateRenderTransform ScreenToWidget = MyGeometry.GetAccumulatedRenderTransform().Inverse();
|
|
const FVector2D DragDelta = ScreenToWidget.TransformVector(FVector2D(DragDropEvent.GetScreenSpacePosition() - Operation->StartPosition));
|
|
|
|
if (Operation->DragType == EDragType::Content)
|
|
{
|
|
// Content offset is in ImGui space, so we need to scale drag calculated in widget space.
|
|
ContentOffset = Operation->StartOffset + DragDelta / CanvasScale;
|
|
}
|
|
else
|
|
{
|
|
// Canvas offset is in widget space, so we can apply drag calculated in widget space directly.
|
|
CanvasOffset = Operation->StartOffset + DragDelta;
|
|
}
|
|
|
|
UpdateRenderTransform();
|
|
|
|
return FReply::Handled();
|
|
}
|
|
else
|
|
{
|
|
return FReply::Unhandled();
|
|
}
|
|
}
|
|
|
|
FReply SImGuiCanvasControl::SImGuiCanvasControl::OnDrop(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent)
|
|
{
|
|
DragRequest = EDragRequest::None;
|
|
return FReply::Handled().ReleaseMouseLock();
|
|
}
|
|
|
|
FVector2D SImGuiCanvasControl::ComputeDesiredSize(float InScale) const
|
|
{
|
|
return FVector2D{ 3840.f, 2160.f } * InScale;
|
|
}
|
|
|
|
namespace
|
|
{
|
|
FORCEINLINE FMargin CalculateInset(const FSlateRect& From, const FSlateRect& To)
|
|
{
|
|
return { To.Left - From.Left, To.Top - From.Top, From.Right - To.Right, From.Bottom - To.Bottom };
|
|
}
|
|
|
|
FORCEINLINE FLinearColor ScaleAlpha(FLinearColor Color, float Scale)
|
|
{
|
|
Color.A *= Scale;
|
|
return Color;
|
|
}
|
|
|
|
FORCEINLINE FVector2D Round(const FVector2D& Vec)
|
|
{
|
|
return FVector2D{ FMath::FloorToFloat(Vec.X), FMath::FloorToFloat(Vec.Y) };
|
|
}
|
|
}
|
|
|
|
int32 SImGuiCanvasControl::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
|
|
{
|
|
const FPaintGeometry PaintGeometry = AllottedGeometry.ToPaintGeometry();
|
|
|
|
const FSlateRenderTransform& WidgetToScreen = AllottedGeometry.GetAccumulatedRenderTransform();
|
|
const FSlateRenderTransform ImGuiToScreen = Transform.Concatenate(WidgetToScreen);
|
|
|
|
const FSlateRect CanvasRect = FSlateRect(
|
|
ImGuiToScreen.TransformPoint(FVector2D::ZeroVector),
|
|
ImGuiToScreen.TransformPoint(ComputeDesiredSize(1.f)));
|
|
const FMargin CanvasMargin = CalculateInset(MyCullingRect, CanvasRect);
|
|
|
|
if (CanvasMargin.GetDesiredSize().SizeSquared() > 0.f)
|
|
{
|
|
CanvasBorderBrush.Margin = CanvasMargin;
|
|
|
|
const FLinearColor CanvasMarginColor = ScaleAlpha(Colors::CanvasMargin, Opacity);
|
|
const FLinearColor CanvasBorderColor = ScaleAlpha(DragRequest == EDragRequest::Content
|
|
? Colors::CanvasBorderHighlight : Colors::CanvasBorder, Opacity);
|
|
|
|
#if ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API
|
|
FSlateDrawElement::MakeBox(OutDrawElements, LayerId, PaintGeometry, &CanvasBorderBrush, MyCullingRect,
|
|
ESlateDrawEffect::None, CanvasMarginColor);
|
|
|
|
FSlateDrawElement::MakeBox(OutDrawElements, LayerId, PaintGeometry, &CanvasBorderBrush,
|
|
CanvasRect.ExtendBy(1).IntersectionWith(MyCullingRect), ESlateDrawEffect::None, CanvasBorderColor);
|
|
|
|
#else
|
|
|
|
FSlateDrawElement::MakeBox(OutDrawElements, LayerId, PaintGeometry, &CanvasBorderBrush, ESlateDrawEffect::None,
|
|
CanvasMarginColor);
|
|
|
|
OutDrawElements.PushClip(FSlateClippingZone{ CanvasRect.ExtendBy(1) });
|
|
FSlateDrawElement::MakeBox(OutDrawElements, LayerId, PaintGeometry, &CanvasBorderBrush, ESlateDrawEffect::None,
|
|
CanvasBorderColor);
|
|
OutDrawElements.PopClip();
|
|
#endif
|
|
}
|
|
|
|
const FSlateRect FrameRect = FSlateRect::FromPointAndExtent(
|
|
WidgetToScreen.TransformPoint(Round(CanvasOffset)),
|
|
Round(MyCullingRect.GetSize() * CanvasScale));
|
|
const FMargin FrameMargin = CalculateInset(MyCullingRect, FrameRect);
|
|
|
|
if (FrameMargin.GetDesiredSize().SizeSquared() > 0.f)
|
|
{
|
|
FrameBorderBrush.Margin = FrameMargin;
|
|
|
|
const FLinearColor FrameBorderColor = ScaleAlpha(DragRequest == EDragRequest::Canvas
|
|
? Colors::FrameBorderHighlight : Colors::FrameBorder, Opacity);
|
|
|
|
#if ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API
|
|
FSlateDrawElement::MakeBox(OutDrawElements, LayerId, PaintGeometry, &FrameBorderBrush,
|
|
FrameRect.ExtendBy(1).IntersectionWith(MyCullingRect), ESlateDrawEffect::None, FrameBorderColor);
|
|
#else
|
|
OutDrawElements.PushClip(FSlateClippingZone{ FrameRect.ExtendBy(1) });
|
|
FSlateDrawElement::MakeBox(OutDrawElements, LayerId, PaintGeometry, &FrameBorderBrush, ESlateDrawEffect::None,
|
|
FrameBorderColor);
|
|
OutDrawElements.PopClip();
|
|
#endif
|
|
}
|
|
|
|
return LayerId;
|
|
}
|
|
|
|
void SImGuiCanvasControl::UpdateVisibility()
|
|
{
|
|
SetVisibility(bActive ? EVisibility::Visible : bBlendingOut ? EVisibility::HitTestInvisible : EVisibility::Hidden);
|
|
}
|
|
|
|
void SImGuiCanvasControl::Zoom(const FGeometry& MyGeometry, const float Delta, const FVector2D& MousePosition)
|
|
{
|
|
// If blending out, then cancel.
|
|
bBlendingOut = false;
|
|
|
|
float OldCanvasScale = CanvasScale;
|
|
|
|
// Normalize scale to make sure that it changes in fixed steps and that we don't accumulate rounding errors.
|
|
// Normalizing before applying delta allows for scales that at the edges are not rounded to the closes step.
|
|
CanvasScale = FMath::RoundToFloat(CanvasScale / ZoomScrollSpeed) * ZoomScrollSpeed;
|
|
|
|
// Update the scale.
|
|
CanvasScale = FMath::Clamp(CanvasScale + Delta, GetMinScale(MyGeometry), 2.f);
|
|
|
|
// Update canvas offset to keep it fixed around pivot point.
|
|
if (CanvasScale != OldCanvasScale && OldCanvasScale != 0.f)
|
|
{
|
|
// Pivot points (in screen space):
|
|
// 1) Around mouse: MousePosition
|
|
// 2) Fixed in top-left corner: MyGeometry.GetLayoutBoundingRect().GetTopLeft()
|
|
// 3) Fixed in centre: MyGeometry.GetLayoutBoundingRect().GetCenter()
|
|
const FVector2D PivotPoint = MyGeometry.GetAccumulatedRenderTransform().Inverse().TransformPoint(MousePosition);
|
|
const FVector2D Pivot = PivotPoint - CanvasOffset;
|
|
|
|
CanvasOffset += Pivot * (OldCanvasScale - CanvasScale) / OldCanvasScale;
|
|
}
|
|
|
|
UpdateRenderTransform();
|
|
}
|
|
|
|
void SImGuiCanvasControl::UpdateRenderTransform()
|
|
{
|
|
const FVector2D RenderOffset = Round(ContentOffset * CanvasScale + CanvasOffset);
|
|
Transform = FSlateRenderTransform(CanvasScale, RenderOffset);
|
|
OnTransformChanged.ExecuteIfBound(Transform);
|
|
}
|
|
|
|
float SImGuiCanvasControl::GetMinScale(const FGeometry& MyGeometry)
|
|
{
|
|
#if FROM_ENGINE_VERSION(4, 17)
|
|
#define GET_BOUNDING_RECT GetLayoutBoundingRect
|
|
#else
|
|
#define GET_BOUNDING_RECT GetClippingRect
|
|
#endif
|
|
|
|
const FVector2D DefaultCanvasSize = MyGeometry.GetAccumulatedRenderTransform().TransformVector(ComputeDesiredSize(1.f));
|
|
const FVector2D WidgetSize = MyGeometry.GET_BOUNDING_RECT().GetSize();
|
|
return FMath::Min(WidgetSize.X / DefaultCanvasSize.X, WidgetSize.Y / DefaultCanvasSize.Y);
|
|
|
|
#undef GET_BOUNDING_RECT
|
|
}
|