#include "externals.hh" #include "syncview.hh" #include "globals.hh" #include "window.hh" #include "syncedit.hh" #define IMGUI_DEFINE_MATH_OPERATORS #include using namespace ebcl; namespace { /*= FAKE TABS HELPER =========================================================*/ bool FakeTab_( char const* const name , const bool disabled , const float width = 0.f ) { using namespace ImGui; if ( disabled ) { PushItemFlag( ImGuiItemFlags_Disabled , true ); PushStyleVar( ImGuiStyleVar_Alpha , GetStyle( ).Alpha * .5f ); } const bool rv( Button( name , ImVec2{ width , 0.f } ) ); if ( disabled ) { PopItemFlag( ); PopStyleVar( ); } return rv; } /*= T_ChangeDurationDialog_ ==================================================*/ class T_ChangeDurationDialog_ : public A_ModalDialog { private: const uint32_t units0_; const float uSize0_; const uint32_t uPerMinute0_; uint32_t units_; float uSize_; uint32_t uPerMinute_; bool scale_{ true }; protected: bool drawDialog( ) noexcept override; void onOK( ) noexcept override; public: T_ChangeDurationDialog_( uint32_t units , float uSize ) noexcept; }; /*----------------------------------------------------------------------------*/ T_ChangeDurationDialog_::T_ChangeDurationDialog_( const uint32_t units , const float uSize ) noexcept : A_ModalDialog{ "Set demo duration...##duration-dialog" } , units0_{ units } , uSize0_{ std::max( 1.f / 60.f , uSize ) } , uPerMinute0_{ uint32_t( ImClamp( roundf( 60.f / uSize0_ ) , 1.f , 3600.f ) ) } , units_{ units0_ } , uSize_{ uSize0_ } , uPerMinute_{ uPerMinute0_ } { setInitialSize( 300.f , 180.f ); } bool T_ChangeDurationDialog_::drawDialog( ) noexcept { using namespace ImGui; int tUnits( units_ ); if ( DragInt( "Duration##units" , &tUnits , .1f , 1 , INT32_MAX , "%.0f unit(s)" ) ) { units_ = uint32_t( std::min( INT32_MAX , std::max( 1 , tUnits ) ) ); } float tDuration( units_ * uSize_ ); if ( DragFloat( "##seconds" , &tDuration , 1.f , .001f , FLT_MAX , "%.3f second(s)" ) ) { units_ = std::min( uint32_t( INT32_MAX ) , std::max( 1u , uint32_t( roundf( tDuration / uSize_ ) ) ) ); } Separator( ); int tUsize( floorf( uSize_ * 1000.f ) ); if ( SliderInt( "Units" , &tUsize , 16 , 2000 , "%.0f ms" ) ) { const float pDur{ uSize_ * units_ }; uSize_ = std::min( 2.f , std::max( 1.f / 60.f , .001f * tUsize ) ); uPerMinute_ = roundf( 60.f / uSize_ ); units_ = uint32_t( roundf( pDur / uSize_ ) ); } int tPerMin( uPerMinute_ ); if ( SliderInt( "Units/minute" , &tPerMin , 30 , 3600 ) ) { const float pDur{ uSize_ * units_ }; uPerMinute_ = std::max( 30u , std::min( 3600u , uint32_t( tPerMin ) ) ); uSize_ = 60.f / uPerMinute_; units_ = uint32_t( roundf( pDur / uSize_ ) ); } if ( uPerMinute0_ == uPerMinute_ ) { PushItemFlag( ImGuiItemFlags_Disabled , true ); PushStyleVar( ImGuiStyleVar_Alpha , GetStyle( ).Alpha * .5f ); } Checkbox( "Scale curves" , &scale_ ); if ( uPerMinute0_ == uPerMinute_ ) { PopItemFlag( ); PopStyleVar( ); } return units_ != units0_ || uPerMinute_ != uPerMinute0_; } void T_ChangeDurationDialog_::onOK( ) noexcept { SyncEditor::SetDuration( units_ , uPerMinute_ != uPerMinute0_ ? uSize_ : uSize0_ , scale_ ); } /*= T_SyncViewImpl_ ==========================================================*/ struct T_SyncViewImpl_ { static constexpr float SeqHeaderHeight = 24; static constexpr float BarWidth = 40; const uint32_t ColFrame{ ImGui::GetColorU32( ImVec4{ 0 , 0 , 0 , .8 } ) }; const uint32_t ColHeader{ ImGui::GetColorU32( ImVec4{ .5 , .5 , .5 , .8 } ) }; const uint32_t ColHeaderText{ ImGui::GetColorU32( ImVec4{ 0 , 0 , 0 , 1 } ) }; const uint32_t ColMain{ ImGui::GetColorU32( ImVec4{ .4 , .4 , .4 , .8 } ) }; const uint32_t ColSelection{ ImGui::GetColorU32( ImVec4{ .8 , 1 , .8 , .2 } ) }; const ImVec2 BtSize{ 20 , 0 }; float zoomLevel{ 0.f }; float startPos{ 0.f }; bool followTime{ true }; bool display( ) noexcept; void computeMetrics( float innerWidth ) noexcept; void sequencerWidget( ) noexcept; void sequencerHeader( ImRect const& bb ) noexcept; void sequencerBody( ImRect const& bb ) noexcept; void displayCurveSelectorWindow( ) noexcept; void displayCurveSelector( ) noexcept; void displayOverrideSelector( ) noexcept; // Misc stuff T_StringBuilder stringBuffer; // XXX damn this shit to fucking hell // Computed metrics float barWidth; float cursorPos; uint32_t startBar; float startBarPos; float timePerBar; float totalPixels; float startPixel; // Zoom area selection bool zoomInProgress{ false }; bool justZoomed{ false }; float firstZoomPixel; float curZoomPixel; // Curve display / edition enum E_SubWindow { SW_NONE , SW_CURVE_SELECTOR , SW_OVERRIDE_SELECTOR , }; E_SubWindow sub{ SW_NONE }; T_KeyValueTable< T_String , bool > sCurves; T_Set< T_String > sOverrides; T_String curveFinder; }; constexpr float T_SyncViewImpl_::BarWidth; /*----------------------------------------------------------------------------*/ bool T_SyncViewImpl_::display( ) noexcept { using namespace ImGui; auto const& dspSize( GetIO( ).DisplaySize ); auto& sync( Globals::Sync( ) ); // Window set-up SetNextWindowSize( ImVec2( dspSize.x , dspSize.y * .34f ) , ImGuiSetCond_Appearing ); SetNextWindowPos( ImVec2( 0 , dspSize.y * .66f ) , ImGuiSetCond_Appearing ); bool displayed{ true }; Begin( "Sequencer" , &displayed , ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollWithMouse ); if ( !displayed ) { End( ); return false; } //---------------------------------------------------------------------- // Toolbar if ( Button( sync.playing( ) ? ICON_FA_STOP : ICON_FA_PLAY , BtSize ) ) { sync.playing( ) = !sync.playing( ) && !sync.finished( ); } if ( IsItemHovered( ) ) { BeginTooltip( ); Text( sync.playing( ) ? "Stop" : "Play" ); EndTooltip( ); } SameLine( ); if ( Button( ICON_FA_BACKWARD , BtSize ) ) { sync.setTime( 0 ); } if ( IsItemHovered( ) ) { BeginTooltip( ); Text( "Rewind to 00:00.000" ); EndTooltip( ); } SameLine( ); VerticalSeparator( ); SameLine( ); Text( ICON_FA_SEARCH ); bool zoomHovered{ IsItemHovered( ) }; SameLine( ); PushItemWidth( 100 ); SliderFloat( "##zoom" , &zoomLevel , 0 , 1 , "%.2f" ); if ( zoomHovered || IsItemHovered( ) ) { BeginTooltip( ); Text( "Zoom level" ); EndTooltip( ); } PopItemWidth( ); SameLine( ); if ( Button( followTime ? ICON_FA_LOCK : ICON_FA_UNLOCK , BtSize ) ) { followTime = !followTime; } if ( IsItemHovered( ) ) { BeginTooltip( ); Text( followTime ? "Follows the current position.\nClick to untie." : "Not tied to the current position.\nClick to follow." ); EndTooltip( ); } SameLine( ); VerticalSeparator( ); SameLine( ); if ( Button( ICON_FA_CLOCK_O , BtSize ) ) { Globals::Window( ).pushDialog( NewOwned< T_ChangeDurationDialog_ >( sync.durationUnits( ) , sync.durationUnitSize( ) ) ); } if ( IsItemHovered( ) ) { BeginTooltip( ); Text( "Change duration and time units." ); EndTooltip( ); } SameLine( ); VerticalSeparator( ); SameLine( ); if ( Button( ICON_FA_LINE_CHART ) ) { const bool displaySelector{ sub == SW_CURVE_SELECTOR || sub == SW_OVERRIDE_SELECTOR }; sub = displaySelector ? SW_NONE : SW_CURVE_SELECTOR; curveFinder = T_String{}; } if ( IsItemHovered( ) ) { BeginTooltip( ); Text( "Select curves or sets thereof to display & edit." ); EndTooltip( ); } //---------------------------------------------------------------------- // Sequencer widget & subwindows BeginChild( "##sequencer" , ImVec2{ 0 , 0 } , false , ImGuiWindowFlags_NoScrollWithMouse ); PushItemWidth( -1 ); sequencerWidget( ); PopItemWidth( ); EndChild( ); End( ); switch ( sub ) { case SW_NONE: break; case SW_CURVE_SELECTOR: case SW_OVERRIDE_SELECTOR: displayCurveSelectorWindow( ); break; } return true; } /*------------------------------------------------------------------------------*/ void T_SyncViewImpl_::sequencerWidget( ) noexcept { using namespace ImGui; const auto width{ CalcItemWidth( ) }; auto* const win( GetCurrentWindow( ) ); const auto seqId{ win->GetID( "##sequencer" ) }; const ImVec2 cPos( win->DC.CursorPos ); const ImVec2 ws( GetWindowContentRegionMax( ) ); auto& style( ImGui::GetStyle( ) ); const ImRect bbHeader{ cPos , cPos + ImVec2( width , SeqHeaderHeight ) }; const ImRect bbDisplay{ ImVec2{ cPos.x , bbHeader.Max.y } , ImVec2{ cPos.x + width , GetWindowPos( ).y + ws.y - style.FramePadding.y * 2 } }; const ImRect bbAll{ bbHeader.Min , bbDisplay.Max }; ItemSize( bbAll , style.FramePadding.y ); if ( !ItemAdd( bbAll , seqId ) ) { return; } const bool hovered{ ItemHoverable( bbAll , seqId ) }; computeMetrics( std::max( 0.f , width - 2.f ) ); BeginGroup( ); const auto hdrId{ win->GetID( "##header" ) }; const auto dspId{ win->GetID( "##display" ) }; PushID( seqId ); if ( ItemAdd( bbHeader , hdrId ) ) { PushID( hdrId ); sequencerHeader( bbHeader ); PopID( ); } if ( bbDisplay.Min.y < bbDisplay.Max.y && ItemAdd( bbDisplay , dspId ) ) { PushID( dspId ); sequencerBody( bbDisplay ); PopID( ); } PopID( ); auto& io( GetIO( ) ); if ( hovered && ( io.MouseDown[ 0 ] || io.MouseDown[ 1 ] ) ) { SetActiveID( seqId , win ); FocusWindow( win ); } const bool active( GetCurrentContext( )->ActiveId == seqId ); if ( hovered && !active && io.MouseWheel != 0 ) { zoomLevel = ImSaturate( zoomLevel + .025 * io.MouseWheel ); } else if ( active ) { if ( io.MouseDown[ 0 ] ) { const float p{ io.MousePos.x - bbAll.Min.x + startPixel }; auto& sync( Globals::Sync( ) ); sync.setTime( p * Globals::Sync( ).duration( ) / totalPixels ); } if ( io.MouseDown[ 1 ] ) { const float p{ io.MousePos.x - bbAll.Min.x + startPixel }; if ( !zoomInProgress ) { firstZoomPixel = p; zoomInProgress = true; } curZoomPixel = p; } else if ( zoomInProgress ) { zoomInProgress = false; justZoomed = true; const auto zMin{ std::min( firstZoomPixel , curZoomPixel ) } , zMax{ std::max( firstZoomPixel , curZoomPixel ) } , diff{ zMax - zMin }; if ( diff > 4 ) { auto& sync( Globals::Sync( ) ); const float u( sync.durationUnits( ) ); startPos = zMin * u / totalPixels; if ( ( width - 2.f ) / u >= BarWidth ) { zoomLevel = 0; } else { const auto length{ std::min( u , diff * u / totalPixels ) }; const auto ppu{ std::min( ( width - 2 ) / length , BarWidth ) }; zoomLevel = ( ppu - BarWidth ) / ( BarWidth - width / u ) + 1; } } } if ( !( io.MouseDown[ 0 ] || io.MouseDown[ 1 ] ) ) { ClearActiveID( ); } } EndGroup( ); } void T_SyncViewImpl_::computeMetrics( const float innerWidth ) noexcept { auto& sync( Globals::Sync( ) ); const uint32_t units{ sync.durationUnits( ) }; zoomLevel = ImSaturate( zoomLevel ); const float zoom1Pixels{ std::max( units * BarWidth , innerWidth ) }; totalPixels = zoomLevel * ( zoom1Pixels - innerWidth ) + innerWidth; const uint32_t totalBars{ [=](){ const float b{ std::max( std::min( totalPixels / BarWidth , float( units ) ) , 1.f ) }; const float mod{ fmod( b , 1.f ) }; return uint32_t( b + ( mod ? ( 1 - mod ) : 0 ) ); }() }; const float unitsPerBar{ float( units ) / totalBars }; barWidth = totalPixels / totalBars; const float absCursorPos{ sync.time( ) * totalPixels / sync.duration( ) }; if ( followTime ) { const float dispUnits{ innerWidth * units / totalPixels }; const float uSize{ sync.durationUnitSize( ) }; const float endPos{ std::min( startPos + dispUnits , float( units ) ) }; startPos = endPos - dispUnits; const float spp{ startPos * totalPixels / units }; const float epp{ endPos * totalPixels / units }; if ( absCursorPos < spp || absCursorPos > epp ) { if ( justZoomed ) { followTime = false; } else { startPos = std::max( 0.f , sync.time( ) / uSize - unitsPerBar * .5f ); } } } const float unadjustedStartPixel{ totalPixels * startPos / units }; if ( unadjustedStartPixel + innerWidth > totalPixels ) { startPos = std::max( 0.f , totalPixels - innerWidth ); } startPixel = totalPixels * startPos / units; startBar = [=](){ const float b{ startPixel * totalBars / totalPixels }; const float mod{ fmod( b , 1.f ) }; return uint32_t( std::max( 0 , int32_t( b - ( mod ? mod : 1 ) ) ) ); }(); startBarPos = startBar * barWidth - startPixel; cursorPos = absCursorPos - startPixel; timePerBar = unitsPerBar * sync.durationUnitSize( ); assert( startBarPos <= 0 ); assert( totalPixels >= innerWidth ); justZoomed = false; } void T_SyncViewImpl_::sequencerHeader( ImRect const& bb ) noexcept { using namespace ImGui; auto* const dl( GetWindowDrawList( ) ); const ImRect inner{ bb.Min + ImVec2{ 1 , 1 } , bb.Max - ImVec2{ 1 , 1 } }; dl->AddRectFilled( inner.Min , inner.Max , ColHeader ); dl->AddRect( bb.Min , bb.Max , ColFrame ); if ( cursorPos >= 0 && cursorPos <= inner.GetWidth( ) ) { auto* const dl( GetWindowDrawList( ) ); dl->AddLine( inner.Min + ImVec2{ cursorPos , 0 } , ImVec2{ inner.Min.x + cursorPos , inner.Max.y - 1 } , GetColorU32( ImVec4{ 1 , 1 , 1 , .5 } ) ); } PushFont( Globals::Window( ).smallFont( ) ); PushStyleColor( ImGuiCol_Text , ColHeaderText ); auto pos{ startBarPos }; auto bar{ startBar }; const auto max{ bb.GetWidth( ) + barWidth - 2.f }; char buffer[ 12 ]; while ( pos < max ) { const ImVec2 taStart{ inner.Min + ImVec2{ pos - barWidth * .5f , 0 } }; const ImVec2 taEnd{ taStart + ImVec2{ barWidth , inner.Max.y - inner.Min.y } }; const float time{ bar * timePerBar }; const float msf{ fmod( time , 1.f ) }; const float sf{ fmod( time - msf , 60.f ) }; snprintf( buffer , sizeof( buffer ) , "%02d:%02d.%03d" , uint32_t( ( time - msf - sf ) / 60.f ) , uint32_t( sf ) , uint32_t( msf * 1000.f ) ); RenderTextClipped( taStart , taEnd , buffer , nullptr , nullptr , ImVec2{ .5f , .2f + ( ( bar % 2 ) ? .6f : 0.f ) } , &inner ); pos += barWidth; bar ++; } PopStyleColor( ); PopFont( ); } void T_SyncViewImpl_::sequencerBody( ImRect const& bb ) noexcept { using namespace ImGui; auto* const dl( GetWindowDrawList( ) ); const ImRect inner{ bb.Min + ImVec2{ 1 , 1 } , bb.Max - ImVec2{ 1 , 1 } }; dl->AddRectFilled( inner.Min , inner.Max , ColMain ); dl->AddRect( bb.Min , bb.Max , ColFrame ); if ( zoomInProgress ) { const float z0{ ImClamp( firstZoomPixel - startPixel + inner.Min.x , inner.Min.x , inner.Max.x ) } , z1{ ImClamp( curZoomPixel - startPixel + bb.Min.x , inner.Min.x , inner.Max.x ) }; const float zMin{ std::min( z0 , z1 ) } , zMax{ std::max( z0 , z1 ) }; if ( zMin != zMax ) { dl->AddRectFilled( ImVec2{ zMin , inner.Min.y } , ImVec2{ zMax , inner.Max.y } , ColSelection ); } } auto pos{ startBarPos }; auto bar{ startBar }; const auto max{ bb.GetWidth( ) + barWidth - 2.f }; while ( pos < max ) { if ( pos >= 0 && pos < inner.GetWidth( ) ) { dl->AddLine( bb.Min + ImVec2{ pos , 0 } , ImVec2{ inner.Min.x + pos , inner.Max.y } , 0xff000000 ); } pos += barWidth; bar ++; } if ( cursorPos >= 0 && cursorPos <= inner.GetWidth( ) ) { auto* const dl( GetWindowDrawList( ) ); dl->AddLine( inner.Min + ImVec2{ cursorPos , -1 } , ImVec2{ inner.Min.x + cursorPos , inner.Max.y - 1 } , 0xffffffff ); } } /*----------------------------------------------------------------------------*/ void T_SyncViewImpl_::displayCurveSelectorWindow( ) noexcept { using namespace ImGui; auto const& dspSize( GetIO( ).DisplaySize ); // Window set-up SetNextWindowSize( ImVec2( dspSize.x * .25f , dspSize.y * .66f - 20 ) , ImGuiSetCond_Appearing ); SetNextWindowPos( ImVec2( dspSize.x * .75f , 20 ) , ImGuiSetCond_Appearing ); bool displayed{ true }; Begin( "Display curves" , &displayed , ImGuiWindowFlags_NoCollapse ); if ( !displayed ) { End( ); sub = SW_NONE; return; } // "Tabs" const ImVec2 ws( GetWindowContentRegionMax( ) ); auto const& style( GetStyle( ) ); const float innerWidth{ ws.x - 2 * style.FramePadding.x }; constexpr float nButtons{ 2.f }; const float buttonWidth{ std::max( 50.f , ( innerWidth - nButtons * style.FramePadding.x ) / nButtons ) }; if ( FakeTab_( "Individual inputs" , sub == SW_CURVE_SELECTOR , buttonWidth ) ) { sub = SW_CURVE_SELECTOR; } SameLine( 0 ); if ( FakeTab_( "Overrides" , sub == SW_OVERRIDE_SELECTOR , buttonWidth ) ) { sub = SW_OVERRIDE_SELECTOR; } // Content switch ( sub ) { case SW_CURVE_SELECTOR: displayCurveSelector( ); break; case SW_OVERRIDE_SELECTOR: displayOverrideSelector( ); break; default: fprintf( stderr , "unexpected bullshit in sync view\n" ); std::abort( ); } End( ); } void T_SyncViewImpl_::displayCurveSelector( ) noexcept { using namespace ImGui; T_Array< T_String > names{ Globals::Sync( ).inputNames( ) }; // Search box; FIXME, this is utterly hacky stringBuffer.clear( ) << curveFinder; while ( stringBuffer.size( ) < 100 ) { stringBuffer << '\0'; } Text( ICON_FA_SEARCH ); SameLine( ); PushItemWidth( -1 ); if ( InputText( "##find" , const_cast< char* >( stringBuffer.data( ) ) , stringBuffer.size( ) ) ) { curveFinder = T_String{ stringBuffer.data( ) , uint32_t( strlen( stringBuffer.data( ) ) ) }; } PopItemWidth( ); if ( curveFinder ) { for ( auto i = 0u ; i < names.size( ) ; ) { auto const& n( names[ i ] ); if ( n.find( curveFinder ) == -1 ) { names.removeSwap( i ); } else { i ++; } } } names.sort( ); // The list ImGui::BeginChild( "content" ); for ( auto const& n : names ) { const bool present{ sCurves.contains( n ) }; const bool overriden{ present && *sCurves.get( n ) }; if ( overriden ) { PushItemFlag( ImGuiItemFlags_Disabled , true ); PushStyleVar( ImGuiStyleVar_Alpha , GetStyle( ).Alpha * .5f ); } bool select{ present }; stringBuffer.clear( ) << n << '\0'; if ( Checkbox( stringBuffer.data( ) , &select ) ) { if ( select ) { sCurves.add( n , false ); } else { sCurves.remove( n ); } } if ( overriden ) { PopItemFlag( ); PopStyleVar( ); } } EndChild( ); } void T_SyncViewImpl_::displayOverrideSelector( ) noexcept { using namespace ImGui; BeginChild( "content" ); Globals::Sync( ).visitOverrides( [&]( T_SyncOverrideVisitor::T_Element element , const bool exit ) { if ( element.hasType< T_SyncOverrideSection* >( ) ) { auto const& sos{ *element.value< T_SyncOverrideSection* >( ) }; if ( sos.title == "*root*" ) { return true; } if ( exit ) { TreePop( ); } else { return TreeNodeEx( &sos.cTitle[ 0 ] , ImGuiTreeNodeFlags_DefaultOpen ); } } else if ( exit ) { auto const& ov{ *element.value< A_SyncOverride* >( ) }; auto const& id{ ov.id( ) }; auto const& in{ ov.inputNames( ) }; const bool present{ sOverrides.contains( id ) }; const bool hasCurves{ !present && [&](){ for ( auto i = 0u ; i < in.size( ) ; i ++ ) { if ( sCurves.contains( in[ i ] ) ) { return true; } } return false; }() }; if ( hasCurves ) { PushItemFlag( ImGuiItemFlags_Disabled , true ); PushStyleVar( ImGuiStyleVar_Alpha , GetStyle( ).Alpha * .5f ); } bool select{ present }; if ( Checkbox( ov.title( ) , &select ) ) { if ( select ) { sOverrides.add( id ); for ( auto i = 0u ; i < in.size( ) ; i ++ ) { sCurves.add( in[ i ] , true ); } } else { sOverrides.remove( id ); for ( auto i = 0u ; i < in.size( ) ; i ++ ) { sCurves.remove( in[ i ] ); } } } if ( hasCurves ) { PopItemFlag( ); PopStyleVar( ); } } return true; } ); EndChild( ); } } // namespace /*= T_SyncView ===============================================================*/ T_SyncView::T_SyncView( ) noexcept : A_PrivateImplementation( new T_SyncViewImpl_( ) ) { } bool T_SyncView::display( ) noexcept { return p< T_SyncViewImpl_ >( ).display( ); }