// Copyright Epic Games, Inc. All Rights Reserved. #include "Framework/Docking/SPanelDrawerArea.h" #include "Containers/Array.h" #include "Framework/Docking/DockingUtilsPrivate.h" #include "Framework/Docking/STabPanelDrawer.h" #include "Layout/Geometry.h" #include "Layout/WidgetPath.h" #include "Math/UnrealMathUtility.h" #include "Templates/SharedPointer.h" #include "Widgets/Docking/SDockTab.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SConstraintCanvas.h" #include "Widgets/Layout/SSplitter.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SNullWidget.h" namespace UE::Slate::Private { constexpr float SSplitterHandleWith = 5.f; constexpr float AnimationLength = 0.15f; void SPanelDrawerArea::Construct(const FArguments& InArgs, const TSharedRef& InContent) { AreaContent = InContent; OpenCloseAnimation = FCurveSequence(0.f, AnimationLength, ECurveEaseFunction::QuadOut); // Add Restoring logic here SetupClosedLayout(); } SPanelDrawerArea::~SPanelDrawerArea() { FSlateThrottleManager::Get().LeaveResponsiveMode(AnimationThrottle); } void SPanelDrawerArea::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { // Compute the desired final size of the panel (Always cached to avoid issue jumping issue) if (PanelDrawerData) { const FTabManager::FPanelDrawerSize& Size = PanelDrawerData->Size; float TotalCoefficient = Size.MainContentCoefficient + Size.PanelDrawerCoefficient; if (TotalCoefficient == 0) { DesiredOpenWidth = (AllottedGeometry.GetLocalSize().X - SSplitterHandleWith) / 2; } else { DesiredOpenWidth = (AllottedGeometry.GetLocalSize().X - SSplitterHandleWith) * Size.PanelDrawerCoefficient / (Size.MainContentCoefficient + Size.PanelDrawerCoefficient); } if (OpenCloseAnimation.IsPlaying()) { UpdateAnimatedSlideWidth(); } } } void SPanelDrawerArea::OpenPanel(bool bWithAnimation, const TSharedRef& InData) { SetPanelDrawerData(InData); if (bWithAnimation) { constexpr bool bIsOpening = true; PlayAnimation(bIsOpening); } else { SetupOpenedLayout(); } if (!bIsOpen) { bIsOpen = true; RequestSaveLayout(); OnExternalStateChange.ExecuteIfBound(); if (!CVarNoAnimationOnTabForegroundedEvent.GetValueOnGameThread()) { if (PanelDrawerData && PanelDrawerData->HostedTab) { TSharedRef GlobalTabManager = FGlobalTabmanager::Get(); TSharedPtr LocalTabManager = PanelDrawerData->HostedTab->GetTabManagerPtr(); if (GlobalTabManager != LocalTabManager) { LocalTabManager->GetPrivateApi().OnTabForegrounded(PanelDrawerData->HostedTab, {}); } GlobalTabManager->GetPrivateApi().OnTabForegrounded(PanelDrawerData->HostedTab, {}); } } } } void SPanelDrawerArea::ClosePanel(bool bWithAnimation, bool bIsTabBeingTransfered) { if (!bIsOpen) { return; } TSharedPtr OldTabDisplayed; if (PanelDrawerData) { OldTabDisplayed = PanelDrawerData->HostedTab; } if (bIsTabBeingTransfered && PanelDrawerData) { TSharedRef NewPanelDrawerData = MakeShared(*PanelDrawerData.Get()); NewPanelDrawerData->HostedTab.Reset(); SetPanelDrawerData(MoveTemp(NewPanelDrawerData)); } if (bWithAnimation) { constexpr bool bIsOpening = false; PlayAnimation(bIsOpening); } else { SetupClosedLayout(); } if (bIsOpen) { RequestSaveLayout(); bIsOpen = false; OnExternalStateChange.ExecuteIfBound(); if (CVarNoAnimationOnTabForegroundedEvent.GetValueOnGameThread() || bIsTabBeingTransfered) { if (OldTabDisplayed) { TSharedRef GlobalTabManager = FGlobalTabmanager::Get(); TSharedPtr LocalTabManager = OldTabDisplayed->GetTabManagerPtr(); if (GlobalTabManager != LocalTabManager) { LocalTabManager->GetPrivateApi().OnTabForegrounded({}, OldTabDisplayed); } GlobalTabManager->GetPrivateApi().OnTabForegrounded({}, OldTabDisplayed); } } } } void SPanelDrawerArea::SetupAnimationLayout() { ChildSlot [ SNew(SSplitter) .Orientation(Orient_Horizontal) + SSplitter::Slot() .Value(bIsOpen ? 1.f : PanelDrawerData->Size.MainContentCoefficient) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .FillWidth(1.f) [ // Add Clipping for content that isn't responsive to dynamic sizes SNew(SBox) .HAlign(HAlign_Fill) .VAlign(VAlign_Fill) .Clipping(EWidgetClipping::ClipToBounds) [ AreaContent.ToSharedRef() ] ] // Fake the splitter handle during the slide, only shown when that part of the splitter should be visible but can't be yet. +SHorizontalBox::Slot() .AutoWidth() [ SNew(SBox) .VAlign(VAlign_Fill) .HAlign(HAlign_Right) .WidthOverride(this, &SPanelDrawerArea::GetAnimatedWidthOverrideForSpacer) .Visibility(this, &SPanelDrawerArea::GetAnimatedSpacerVisibility) ] ] + SSplitter::Slot() .Value(bIsOpen ? 0.0f : PanelDrawerData->Size.PanelDrawerCoefficient) .SizeRule(SSplitter::ESizeRule::SizeToContent) [ SNew(SBox) .HAlign(HAlign_Left) .VAlign(VAlign_Fill) .WidthOverride(this, &SPanelDrawerArea::GetAnimatedWidthOverrideForPanelDrawer) .Visibility(this, &SPanelDrawerArea::GetAnimatedDrawerPanelVisibility) .Padding(FMargin(0.0, 0.0, 0.0, 2.0)) .Clipping(EWidgetClipping::ClipToBounds) [ SNew(SConstraintCanvas) + SConstraintCanvas::Slot() .Anchors(FAnchors(0.f, 0.f, 0.f, 1.f)) .Offset(FMargin(0.f, 0.f, DesiredOpenWidth, 0.f)) .Alignment(FVector2D(0.f, 0.f)) [ PanelDrawerContent.ToSharedRef() ] ] ] ]; } void SPanelDrawerArea::SetupClosedLayout() { ChildSlot [ AreaContent.ToSharedRef() ]; if (!CVarNoAnimationOnTabForegroundedEvent.GetValueOnGameThread()) { if (PanelDrawerData && PanelDrawerData->HostedTab) { TSharedRef GlobalTabManager = FGlobalTabmanager::Get(); TSharedPtr LocalTabManager = PanelDrawerData->HostedTab->GetTabManagerPtr(); if (GlobalTabManager != LocalTabManager) { LocalTabManager->GetPrivateApi().OnTabForegrounded({}, PanelDrawerData->HostedTab); } GlobalTabManager->GetPrivateApi().OnTabForegrounded({}, PanelDrawerData->HostedTab); } } SetPanelDrawerData({}); } void SPanelDrawerArea::SetupOpenedLayout() { ChildSlot [ SNew(SSplitter) .Orientation(Orient_Horizontal) + SSplitter::Slot() .OnSlotResized(this, &SPanelDrawerArea::OnMainContentSlotResized) .Value(this, &SPanelDrawerArea::GetMainContentCoefficient) [ // Add Clipping for content that isn't responsive to dynamic sizes SNew(SBox) .HAlign(HAlign_Fill) .VAlign(VAlign_Fill) .Clipping(EWidgetClipping::ClipToBounds) [ AreaContent.ToSharedRef() ] ] + SSplitter::Slot() .OnSlotResized(this, &SPanelDrawerArea::OnPanelDrawerSlotResized) .Value(this, &SPanelDrawerArea::GetPanelDrawerCoefficient) [ // Add Clipping for content that isn't responsive to dynamic sizes SNew(SBox) .HAlign(HAlign_Fill) .VAlign(VAlign_Fill) .Padding(FMargin(0.0, 0.0, 0.0, 2.0)) .Clipping(EWidgetClipping::ClipToBounds) [ PanelDrawerContent.ToSharedRef() ] ] ]; if (CVarNoAnimationOnTabForegroundedEvent.GetValueOnGameThread()) { if (PanelDrawerData && PanelDrawerData->HostedTab) { TSharedRef GlobalTabManager = FGlobalTabmanager::Get(); TSharedPtr LocalTabManager = PanelDrawerData->HostedTab->GetTabManagerPtr(); if (GlobalTabManager != LocalTabManager) { LocalTabManager->GetPrivateApi().OnTabForegrounded(PanelDrawerData->HostedTab, {}); } GlobalTabManager->GetPrivateApi().OnTabForegrounded(PanelDrawerData->HostedTab, {}); } } } void SPanelDrawerArea::OnMainContentSlotResized(float InMainContentCoefficient) { PanelDrawerData->Size.MainContentCoefficient = InMainContentCoefficient; RequestSaveLayout(); } void SPanelDrawerArea::OnPanelDrawerSlotResized(float InPanelDrawerCoefficient) { PanelDrawerData->Size.PanelDrawerCoefficient = InPanelDrawerCoefficient; RequestSaveLayout(); } float SPanelDrawerArea::GetMainContentCoefficient() const { return PanelDrawerData->Size.MainContentCoefficient; } float SPanelDrawerArea::GetPanelDrawerCoefficient() const { return PanelDrawerData->Size.PanelDrawerCoefficient; } void SPanelDrawerArea::PlayAnimation(bool bIsOpening) { if (OpenCloseAnimation.IsPlaying()) { OpenCloseAnimation.PlayRelative(AsShared(), bIsOpening); } else { constexpr bool bIsLooped = false; constexpr float StartTime = 0.f; constexpr bool bRequireActiveTimer = false; if (bIsOpening) { OpenCloseAnimation.Play(AsShared(), bIsLooped, StartTime, bRequireActiveTimer); } else { OpenCloseAnimation.PlayReverse(AsShared(), bIsLooped, StartTime, bRequireActiveTimer); } UpdateAnimatedSlideWidth(); SetupAnimationLayout(); AnimationThrottle = FSlateThrottleManager::Get().EnterResponsiveMode(); OpenCloseTimer = RegisterActiveTimer(0.f, FWidgetActiveTimerDelegate::CreateSP(this, &SPanelDrawerArea::UpdateAnimation)); } } EActiveTimerReturnType SPanelDrawerArea::UpdateAnimation(const double InCurrentTime, const float InDeltaTime) { if (!OpenCloseAnimation.IsPlaying()) { if (bIsOpen) { SetupOpenedLayout(); } else { SetupClosedLayout(); } FSlateThrottleManager::Get().LeaveResponsiveMode(AnimationThrottle); OpenCloseTimer.Reset(); return EActiveTimerReturnType::Stop; } return EActiveTimerReturnType::Continue; } void SPanelDrawerArea::UpdateAnimatedSlideWidth() { AnimatedSlideWidth = FMath::Lerp(0.f, DesiredOpenWidth + SSplitterHandleWith, OpenCloseAnimation.GetLerp()); } FOptionalSize SPanelDrawerArea::GetAnimatedWidthOverrideForSpacer() const { return FMath::Min(5.f, AnimatedSlideWidth); } FOptionalSize SPanelDrawerArea::GetAnimatedWidthOverrideForPanelDrawer() const { return FMath::Max(0.f, AnimatedSlideWidth - 5.f); } EVisibility SPanelDrawerArea::GetAnimatedSpacerVisibility() const { return AnimatedSlideWidth >= 5.f ? EVisibility::Collapsed : EVisibility::Visible; } EVisibility SPanelDrawerArea::GetAnimatedDrawerPanelVisibility() const { return AnimatedSlideWidth < 5.f ? EVisibility::Collapsed : EVisibility::Visible; } bool SPanelDrawerArea::IsOpen() const { return bIsOpen; } bool SPanelDrawerArea::IsHostingTab(TSharedPtr InDockTab) const { if (PanelDrawerData) { return InDockTab == PanelDrawerData->HostedTab; } return false; } FSimpleDelegate& SPanelDrawerArea::GetOnExternalStateChanged() { return OnExternalStateChange; } TSharedPtr SPanelDrawerArea::GetHostedTab() const { if (PanelDrawerData) { return PanelDrawerData->HostedTab; } return {}; } TSharedPtr SPanelDrawerArea::GetHostedPanelDrawerData() const { if (bIsOpen) { return PanelDrawerData; } return {}; } void SPanelDrawerArea::RequestSaveLayout() { if (PanelDrawerData && PanelDrawerData->HostedTab) { if (TSharedPtr TabManager = PanelDrawerData->HostedTab->GetTabManagerPtr()) { TabManager->RequestSavePersistentLayout(); } } } TSharedRef SPanelDrawerArea::MakePanelDrawerContent() const { if (PanelDrawerData && PanelDrawerData->HostedTab) { return SNew(SVerticalBox) + SVerticalBox::Slot() .HAlign(HAlign_Fill) .AutoHeight() .MinHeight(27.f) [ SNew(UE::Slate::Private::STabPanelDrawer, PanelDrawerData->HostedTab.ToSharedRef()) ] + SVerticalBox::Slot() .FillHeight(1.f) [ PanelDrawerData->HostedTab->GetContent() ]; } return SNullWidget::NullWidget; } void SPanelDrawerArea::SetPanelDrawerData(TSharedPtr&& InNewData) { PanelDrawerData = MoveTemp(InNewData); PanelDrawerContent = MakePanelDrawerContent(); } void SPanelDrawerArea::OnFocusChanging(const FWeakWidgetPath& PreviousFocusPath, const FWidgetPath& NewWidgetPath, const FFocusEvent& InFocusEvent) { if (PanelDrawerContent && PanelDrawerData && PanelDrawerData->HostedTab) { if (IsOpen()) { const bool bIsDrawerPanelContentFocused = NewWidgetPath.ContainsWidget(PanelDrawerContent.Get()); if (bIsDrawerPanelContentFocused) { // If a widget inside this tab stack got focused, activate this tab. FGlobalTabmanager::Get()->SetActiveTab(PanelDrawerData->HostedTab); PanelDrawerData->HostedTab->ActivateInParent(ETabActivationCause::SetDirectly); } } } } }