Files
GTASource/game/frontend/page_deck/uiPageTransitioner.cpp
expvintl 419f2e4752 init
2025-02-23 17:40:52 +08:00

477 lines
15 KiB
C++

/////////////////////////////////////////////////////////////////////////////////
//
// FILE : uiPageTransitioner.cpp
// PURPOSE : Handles moving between CPageBases and updating a stack of
// active parentage.
//
// AUTHOR : james.strain
// STARTED : December 2020
//
/////////////////////////////////////////////////////////////////////////////////
#include "uiPageTransitioner.h"
#if UI_PAGE_DECK_ENABLED
// game
#include "frontend/page_deck/PageBase.h"
#include "frontend/ui_channel.h"
FRONTEND_OPTIMISATIONS();
uiPageTransitioner::uiPageTransitioner()
: m_pageStack()
, m_transitionTimeout( 0 )
{
}
void uiPageTransitioner::AbandonTransition()
{
if( HasTransition() )
{
CPageBase * const currentTarget = GetCurrentTransitionTarget();
if( !IsStacked( currentTarget ) )
{
currentTarget->ExitPage( true );
}
}
CleanupTransitionInfo();
}
void uiPageTransitioner::Shutdown()
{
m_transitionCollection.ResetCount();
AbandonTransition();
UnstackAllPages();
}
bool uiPageTransitioner::PushTransition( uiPageLink const& pageLink, uiPageConfig::PageMap const& pageMap )
{
bool result = false;
// Retrieve list of pages in transition
uiPageConfig::ConstPageCollection transitionSteps;
uiPageConfig::PageInfoCollection const& c_transitionStepsInfo = pageLink.GetTransitionSteps();
int const c_transitionStepsCount = c_transitionStepsInfo.GetCount();
for( int i = 0; i < c_transitionStepsCount; ++i )
{
uiPageInfo const& c_pageInfo = c_transitionStepsInfo[i];
uiPageId c_pageId = c_pageInfo.GetId();
CPageBase* const * const nestedTransitionPageEntry = pageMap.Access( c_pageId );
if( IsPageValidForTransition( nestedTransitionPageEntry, c_pageId ) )
{
transitionSteps.PushAndGrow( *nestedTransitionPageEntry );
}
}
// Only add this transition if all pages were found and valid
if ( transitionSteps.GetCount() == c_transitionStepsCount )
{
CPageBase * const sourcePage = GetSourceForTransition();
m_transitionCollection.PushAndGrow( TransitionInfo( sourcePage, transitionSteps, pageLink ) );
}
result = HasTransition();
return result;
}
bool uiPageTransitioner::TransitionToParent()
{
bool result = false;
CPageBase * const sourcePage = m_pageStack.GetCount() > 0 ? m_pageStack.Top() : nullptr;
int const c_pageCount = m_pageStack.GetCount();
if( c_pageCount > 1 )
{
CPageBase* parentPage = nullptr;
int steps = 1;
for( int index = c_pageCount - 2; parentPage == nullptr && index >= 0; --index )
{
CPageBase* const potentialParent = m_pageStack[index];
if( !potentialParent->IsTransitoryPage() )
{
parentPage = potentialParent;
break;
}
++steps;
}
if( uiVerifyf( parentPage, "Unable to find suitable parent page for transition request" ) )
{
uiPageLink const c_parentPageLink(parentPage->GetId());
m_transitionCollection.PushAndGrow( TransitionInfo( sourcePage, parentPage, c_parentPageLink, steps ) );
result = true;
}
}
return result;
}
bool uiPageTransitioner::StepTransition( float const deltaMs )
{
CPageBase * currentTargetPage = GetCurrentTransitionTarget();
// People visiting from the future, this function is a cutdown version of fwuiNodeTransitionHandler::StepTransition
// from future projects. It was ratified in RDR development, but sliced down here to just the logic we need for this backport
if( currentTargetPage == nullptr )
{
return false;
}
CPageBase * currentSourcePage = GetCurrentTransitionSource();
bool const c_isBackwardTransition = IsTransitionToParent();
m_transitionTimeout += deltaMs;
const float c_sourceExitTimeout = currentSourcePage && !currentSourcePage->IsExitComplete() ? currentSourcePage->GetTransitionTimeout() : 0.0f;
const float c_targetEnterTimeout = currentTargetPage && !currentTargetPage->IsEnteringComplete() ? currentTargetPage->GetTransitionTimeout() : 0.0f;
const float c_timeoutLimit = rage::Max( c_sourceExitTimeout, c_targetEnterTimeout );
bool const c_forceTransitionThisFrame = c_timeoutLimit > 0.0f && m_transitionTimeout >= c_timeoutLimit;
bool const c_immediateLink = c_forceTransitionThisFrame || IsImmediateLink();
ASSERT_ONLY( uiPageId const c_sourceID = currentSourcePage ? currentSourcePage->GetId() : uiPageId() );
ASSERT_ONLY( uiPageId const c_targetID = currentTargetPage ? currentTargetPage->GetId() : uiPageId() );
uiAssertf( !c_forceTransitionThisFrame, "Forcing transition this frame from '" HASHFMT "', to '" HASHFMT "', after %.1fms timeout.",
HASHOUT( c_sourceID ), HASHOUT( c_targetID ), c_timeoutLimit );
bool exitedThisFrame = c_forceTransitionThisFrame;
bool enteredThisFrame = c_forceTransitionThisFrame;
// We check m_currentTransitionTarget and m_currentTransitionSource as exit/update/enter/update occurs as the
// states may be re-entrant and interact with the flow, so we may have been cleaned up mid-transition
if( currentSourcePage )
{
bool const c_wasExited = currentSourcePage->IsExitComplete();
if( currentSourcePage->CanExit() || c_forceTransitionThisFrame )
{
if( currentSourcePage->IsFocused() )
{
currentSourcePage->LoseFocus();
}
if( c_isBackwardTransition )
{
currentSourcePage->ExitPage( c_immediateLink );
}
}
if( currentSourcePage->IsExiting() )
{
currentSourcePage->Update( deltaMs );
}
exitedThisFrame = !currentSourcePage || !c_isBackwardTransition ||
( !c_wasExited && currentSourcePage->IsExitComplete() ) || c_forceTransitionThisFrame;
}
else
{
exitedThisFrame = true;
}
if( currentTargetPage &&
( !currentSourcePage || currentSourcePage->IsExitComplete() || !c_isBackwardTransition || c_forceTransitionThisFrame ) )
{
if( currentTargetPage->ShouldEnter() )
{
currentTargetPage->EnterPage( c_immediateLink );
}
if( currentTargetPage && ( currentTargetPage->IsEntering() || !c_isBackwardTransition ) )
{
currentTargetPage->Update( deltaMs );
}
if( currentTargetPage && ( currentTargetPage->IsEnteringComplete() || c_forceTransitionThisFrame ) )
{
if( !currentTargetPage->IsEnteringComplete() )
{
// If we didn't finish entering, give a final nudge to the state. This will help figuring out what state to transition next.
// Note that this nudge is not important as important when OnExiting, since for that case we will have decided a transition target.
currentTargetPage->OnEnterTimedOut();
}
UpdateTransitionTarget();
enteredThisFrame = true;
}
}
else
{
enteredThisFrame = true;
// If we exited the source node this frame and there's no target, means we finished transitioning
if( exitedThisFrame && !HasTransition() )
{
CleanupTransitionInfo();
}
}
// We want externals to treat our transition as immediate if we entered and exited on the same frame,
// or the link type says so
bool const c_wasImmediateTransitionThisFrame = ( exitedThisFrame && enteredThisFrame ) || c_immediateLink;
return c_wasImmediateTransitionThisFrame;
}
bool uiPageTransitioner::HasTransition() const
{
return m_transitionCollection.size() > 0;
}
bool uiPageTransitioner::HasTransition( uiPageLink const& pageLink ) const
{
bool found = false;
int const c_count = m_transitionCollection.GetCount();
for( int index = 0; found == false && index < c_count; ++index )
{
TransitionInfo const * const c_info = &m_transitionCollection[index];
if( c_info->m_pageLink == pageLink )
{
found = true;
break;
}
}
return found;
}
bool uiPageTransitioner::CanBackOut() const
{
bool result = false;
auto func = [&result]( uiPageId const UNUSED_PARAM(id), CPageBase const& currentPage )
{
if( !result )
{
result = !currentPage.IsTransitoryPage();
}
};
ForEachStackedPage_ExceptFocused( func );
return result;
}
void uiPageTransitioner::ForEachStackedPage_ExceptFocused( uiPageConfig::PageVisitorLambda action )
{
int const c_count = m_pageStack.GetCount();
for( int index = 0; index < c_count - 1; ++index )
{
CPageBase * const currentPage = m_pageStack[index];
action( currentPage->GetId(), *currentPage );
}
}
void uiPageTransitioner::ForEachStackedPage_ExceptFocused( uiPageConfig::PageVisitorConstLambda action ) const
{
int const c_count = m_pageStack.GetCount();
for( int index = 0; index < c_count - 1; ++index )
{
CPageBase const * const c_currentPage = m_pageStack[index];
action( c_currentPage->GetId(), *c_currentPage );
}
}
void uiPageTransitioner::CleanupTransitionInfo()
{
if( m_transitionCollection.GetCount() > 0 )
{
m_transitionCollection.Delete( 0 );
}
// Validate any remaining transitions can still be acted on
for( int index = 0; index < m_transitionCollection.GetCount(); )
{
TransitionInfo const * const c_info = &m_transitionCollection[index];
if( !IsStacked( c_info->m_transitionSource ))
{
m_transitionCollection.Delete( index );
}
else
{
++index;
}
}
m_transitionTimeout = 0;
}
CPageBase* uiPageTransitioner::GetSourceForTransition()
{
// If we have no pages stacked, we've likely requested a transition during the enter
// of our first page (before it's stacked)
// so use the transition target instead
CPageBase* result = m_pageStack.GetCount() > 0 ? m_pageStack.Top() : nullptr;
if( result == nullptr )
{
result = GetCurrentTransitionTarget();
}
return result;
}
CPageBase const * uiPageTransitioner::GetCurrentTransitionSource() const
{
CPageBase const * result = nullptr;
if( m_transitionCollection.GetCount() > 0 )
{
TransitionInfo const& c_info = m_transitionCollection[0];
if( c_info.IsTransitionToParent() )
{
result = m_pageStack.Top();
}
else
{
// If not going to parent, we just use it as is
result = c_info.m_transitionSource;
}
}
return result;
}
CPageBase const * uiPageTransitioner::GetCurrentTransitionTarget() const
{
CPageBase const * result = nullptr;
if( m_transitionCollection.GetCount() > 0 )
{
TransitionInfo const& c_info = m_transitionCollection[0];
if( c_info.IsTransitionToParent() )
{
// We are after the _current_ target, not the _final_ target,
// and since we pop nodes off the stack as we move up, a parent transition
// will always be from lastItem to lastItem - 1
int const c_count = m_pageStack.GetCount();
result = c_count > 1 ? m_pageStack[c_count - 2] : nullptr;
}
else
{
// If not going to parent, follow nested transitions
int const c_transitionStepsCount = c_info.m_transitionSteps.GetCount();
if ( uiVerifyf( c_info.m_stepsRemaining <= c_transitionStepsCount, "Invalid number of steps in nested page transition: %d / %d", c_info.m_stepsRemaining, c_transitionStepsCount ) )
{
int const c_pageIndex = c_transitionStepsCount - c_info.m_stepsRemaining;
result = c_info.m_transitionSteps[c_pageIndex];
}
}
}
return result;
}
bool uiPageTransitioner::IsImmediateLink() const
{
bool const c_result = !m_transitionCollection.empty() ?
m_transitionCollection[0].IsNestedTransition() : false;
return c_result;
}
bool uiPageTransitioner::IsTransitionToParent() const
{
bool const c_result = !m_transitionCollection.empty() ?
m_transitionCollection[0].IsTransitionToParent() : false;
return c_result;
}
int uiPageTransitioner::GetStackedIndex( CPageBase const * const page ) const
{
int const c_index = m_pageStack.Find( const_cast<CPageBase*>(page) );
return c_index;
}
bool uiPageTransitioner::IsOnTopOfStack( CPageBase const * const page ) const
{
bool const c_top = !m_pageStack.empty() && m_pageStack.Top() == page;
return c_top;
}
bool uiPageTransitioner::IsParent( CPageBase const * const potentialParent, CPageBase const * const potentialChild ) const
{
int const c_parentIndex = m_pageStack.Find( const_cast<CPageBase*>(potentialParent) );
int const c_childIndex = m_pageStack.Find( const_cast<CPageBase*>(potentialChild) );
return c_parentIndex >= 0 && ( c_parentIndex == c_childIndex - 1);
}
void uiPageTransitioner::UpdateTransitionTarget()
{
if( HasTransition() )
{
TransitionInfo& transitionInfo = m_transitionCollection[0];
// Pull these first before we manipulate the stack
CPageBase * const currentTransitionSource = GetCurrentTransitionSource();
CPageBase * const currentTransitionTarget = GetCurrentTransitionTarget();
bool const c_isForwardTransition = !transitionInfo.IsTransitionToParent();
// If we just left the top of the stack, remove the item
if( currentTransitionSource )
{
if( !c_isForwardTransition )
{
m_pageStack.Pop();
}
}
if( currentTransitionTarget )
{
if( !IsStacked( currentTransitionTarget) )
{
m_pageStack.PushAndGrow( currentTransitionTarget );
}
bool const c_isFinalDestination = transitionInfo.IsFinalDestination();
if( c_isFinalDestination )
{
currentTransitionTarget->GainFocus( c_isForwardTransition );
}
}
if( transitionInfo.StepTransition() )
{
CleanupTransitionInfo();
}
}
}
void uiPageTransitioner::UnstackAllPages()
{
while( m_pageStack.GetCount() )
{
CPageBase * currentPage = m_pageStack.Pop();
if( currentPage->IsFocused() )
{
currentPage->LoseFocus();
}
if( currentPage->ShouldExit() )
{
currentPage->ExitPage( true );
}
}
}
bool uiPageTransitioner::IsPageValidForTransition(CPageBase* const * const page, uiPageId pageId)
{
#if !__ASSERT
(void)pageId;
#endif
bool const c_isValid = uiVerifyf( page != nullptr, "Requested page '" HASHFMT "' but it doesn't exist", HASHOUT( pageId ) )
&& uiVerifyf( *page != nullptr, "Requested page '" HASHFMT "' but it is null", HASHOUT( pageId ) )
&& uiVerifyf( !IsStacked( **page ), "Page '" HASHFMT "' is already active. We do not support instancing at this time.", HASHOUT( pageId ) );
return c_isValid;
}
#endif // UI_PAGE_DECK_ENABLED