demotool/ui-sequencer.cc

1803 lines
47 KiB
C++

#include "externals.hh"
#include "common.hh"
#include "c-sync.hh"
#include "c-syncedit.hh"
#include "ui.hh"
#include "ui-app.hh"
#include "ui-sequencer.hh"
#include "ui-utilities.hh"
#include "ui-imgui-sdl.hh"
#define IMGUI_DEFINE_MATH_OPERATORS
#include <imgui_internal.h>
using namespace ebcl;
namespace {
/*= VARIOUS HELPERS ==========================================================*/
bool FakeTab_(
char const* const name ,
const bool disabled ,
const float width = 0.f )
{
using namespace ImGui;
if ( disabled ) {
PushDisabled( );
}
const bool rv( Button( name , ImVec2{ width , 0.f } ) );
if ( disabled ) {
PopDisabled( );
}
return rv;
}
void TimeToString_(
char* const buffer ,
const size_t bSize ,
const float time ) noexcept
{
const float msf{ fmod( time , 1.f ) };
const float sf{ fmod( time - msf , 60.f ) };
snprintf( buffer , bSize , "%02d:%02d.%03d" ,
uint32_t( ( time - msf - sf ) / 60.f ) ,
uint32_t( sf ) , uint32_t( msf * 1000.f ) );
}
/*= 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:
uint8_t drawDialog( ) noexcept override;
bool onButton( uint8_t id ) 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 );
addButton( "OK" );
addButton( "Cancel" );
}
uint8_t 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_ ) {
PushDisabled( );
}
Checkbox( "Scale curves" , &scale_ );
if ( uPerMinute0_ == uPerMinute_ ) {
PopDisabled( );
}
const bool eo{ units_ != units0_ || uPerMinute_ != uPerMinute0_ };
return eo ? 3 : 2;
}
bool T_ChangeDurationDialog_::onButton(
const uint8_t button ) noexcept
{
if ( button == 0 ) {
SyncEditor::SetDuration( units_ ,
uPerMinute_ != uPerMinute0_ ? uSize_ : uSize0_ ,
scale_ );
}
return true;
}
/*= T_SyncViewImpl_ ==========================================================*/
struct T_SyncViewImpl_
{
static constexpr float SeqHeaderHeight = 24.f;
static constexpr float BarWidth = 40.f;
static constexpr float TrackHeight = 15.f;
static constexpr float TrackPadding = 2.f;
static constexpr float PointRadius = ( TrackHeight - 2.f ) * .5f;
static constexpr float PointRadiusSqr = PointRadius * PointRadius;
bool display( ) noexcept;
private:
// Track display data
struct T_TrackDisplay
{
T_String id;
bool isOverride;
ImRect area;
uint32_t dispSegs;
uint32_t firstSeg;
};
struct T_TrackSegDisplay
{
uint32_t track;
uint32_t seg;
ImRect area;
uint32_t dispPoints;
uint32_t firstPoint;
};
struct T_TrackPointDisplay
{
uint32_t seg;
uint32_t index;
ImVec2 center;
enum {
START ,
MIDDLE ,
END
} mode;
};
// Description for mouse locations in the sequencer window
enum class E_MousePosType
{
NONE ,
TRACK ,
SEGMENT ,
POINT
};
struct T_MousePos
{
E_MousePosType type;
uint32_t index;
};
// Type of sub-windows
enum E_SubWindow {
SW_NONE ,
SW_INPUT_SELECTOR ,
SW_OVERRIDE_SELECTOR ,
SW_TRACK ,
SW_SEGMENT ,
SW_POINT ,
};
// Type of change in progress
enum class E_ChangeType {
NONE ,
POINT_DND ,
POINT_VALUE ,
};
// Make sure all displayed inputs/overrides still exist
void checkTracks( ) noexcept;
// Make sure the currently selected track/segment/point is still valid
void checkSelection( ) noexcept;
// Display the toolbar
void displayToolbar( ) noexcept;
//----------------------------------------------------------------------
// Sequencer widget methods
void sequencerWidget( ) noexcept;
void computeMetrics( float innerWidth ) noexcept;
void sequencerHeader( ImRect const& bb ) noexcept;
void sequencerBody( ImRect const& bb ) noexcept;
void sequencerTracks(
float& hue ,
ImRect& bb ,
ImRect const& container ) noexcept;
void sequencerTrack(
float& hue ,
ImRect const& bb ,
ImRect const& container ,
T_String const& name ,
T_SyncCurve const* curve ) noexcept;
void displayTooltips(
const float time ) noexcept;
bool handlePointDrag(
const float mPixels ,
bool mouseDown ) noexcept;
T_MousePos getMousePos( ) const noexcept;
//----------------------------------------------------------------------
// Track selector
void displayTrackSelectorWindow( ) noexcept;
void displayInputSelector( ) noexcept;
void displayOverrideSelector( ) noexcept;
// Selection display/edition windows
void displayTrackWindow( ) noexcept;
void displaySegmentWindow( ) noexcept;
void displayPointWindow( ) noexcept;
//----------------------------------------------------------------------
// Colors, sizes, etc.
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 uint32_t ColPointNormal{ ImGui::GetColorU32( ImVec4{ 0 , 0 , 0 , .25 } ) };
const uint32_t ColPointSelected{ ImGui::GetColorU32( ImVec4{ 0 , 0 , 0 , .75 } ) };
const ImVec2 BtSize{ 20 , 0 };
// Sequencer settings
float zoomLevel{ 0.f };
float startPos{ 0.f };
bool followTime{ true };
// Misc stuff
T_StringBuilder stringBuffer; // XXX damn this shit to fucking hell
// Computed metrics
bool checkLockMode{ false };
float vScroll{ 0 };
float barWidth;
float cursorPos;
uint32_t startBar;
float startBarPos;
float timePerBar;
float totalPixels;
float startPixel;
// Track display
T_Array< T_TrackDisplay > dspTracks;
T_Array< T_TrackSegDisplay > dspSegments;
T_Array< T_TrackPointDisplay > dspPoints;
// Zoom area selection
bool zoomInProgress{ false };
float firstZoomPixel;
float curZoomPixel;
// Selected item
T_String selId{ };
bool selIsOverride;
T_Optional< uint32_t > selSegment;
T_Optional< uint32_t > selPoint;
bool selPointDnD{ false };
float selPointDnDStart , selPointDnDCur;
// Original and copy of curve being modified
E_ChangeType selUpdate{ E_ChangeType::NONE };
T_Optional< T_SyncCurve > selUpdatingOriginal;
T_Optional< T_SyncCurve > selUpdatingCopy;
// Sub-windows
E_SubWindow sub{ SW_NONE };
// Curve selection
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 );
// 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;
}
checkTracks( );
checkSelection( );
displayToolbar( );
// Sequencer widget
PushItemWidth( -1 );
sequencerWidget( );
PopItemWidth( );
End( );
// Close sub-window if e.g. selection has changed
switch( sub ) {
case SW_POINT:
if ( !selPoint ) {
sub = SW_SEGMENT;
}
// fallthrough
case SW_SEGMENT:
if ( !selSegment ) {
sub = SW_TRACK;
}
// fallthrough
case SW_TRACK:
if ( !selId ) {
sub = SW_NONE;
}
break;
default: break;
}
// Display selected sub-window
switch ( sub ) {
case SW_NONE:
break;
case SW_INPUT_SELECTOR:
case SW_OVERRIDE_SELECTOR:
displayTrackSelectorWindow( );
break;
case SW_TRACK:
displayTrackWindow( );
break;
case SW_SEGMENT:
displaySegmentWindow( );
break;
case SW_POINT:
displayPointWindow( );
break;
}
return true;
}
void T_SyncViewImpl_::checkTracks( ) noexcept
{
auto& sync{ Common::Sync( ) };
// Check for "dead" overrides
{
bool ovRemoved{ false };
for ( auto i = 0u ; i < sOverrides.size( ) ; ) {
if ( sync.overrideExists( sOverrides[ i ] ) ) {
i ++;
} else {
sOverrides.remove( sOverrides[ i ] );
ovRemoved = true;
}
}
if ( !ovRemoved ) {
return;
}
}
// Remove all curves that come from overrides
for ( auto i = 0u ; i < sCurves.size( ) ; ) {
if ( sCurves.values( )[ i ] ) {
sCurves.remove( sCurves.keys( )[ i ] );
} else {
i ++;
}
}
// Re-add curves for the remaining overrides
const auto no{ sOverrides.size( ) };
for ( auto i = 0u ; i < no ; i ++ ) {
auto const* od{ sync.getOverride( sOverrides[ i ] ) };
assert( od );
const auto ni{ od->inputNames( ).size( ) };
for ( auto j = 0u ; j < ni ; j ++ ) {
const bool ok{ sCurves.add( od->inputNames( )[ j ] , true ) };
assert( ok ); (void) ok;
}
}
}
void T_SyncViewImpl_::checkSelection( ) noexcept
{
if ( !selId ) {
return;
}
assert( !selIsOverride ); // XXX
auto const* const curve{ Common::Sync( ).getCurve( selId ) };
// Missing curve
if ( !curve ) {
// Remove segment/point selection
if ( selSegment ) {
selSegment.clear( );
selPoint.clear( );
}
// If there's no matching input, unselect the track
if ( !Common::Sync( ).hasInput( selId ) ) {
selId = T_String{};
}
} else {
// No segment selected? We're ok.
if ( !selSegment ) {
return;
}
// Check segment and point
if ( *selSegment >= curve->segments.size( ) ) {
selSegment.clear( );
selPoint.clear( );
} else if ( selPoint && *selPoint >= curve->segments[
*selSegment ].values.size( ) ) {
selPoint.clear( );
} else {
return;
}
}
// If we were doing something with the curve, get rid of that too
if ( selUpdate != E_ChangeType::NONE ) {
selUpdate = E_ChangeType::NONE;
selUpdatingCopy.clear( );
selUpdatingOriginal.clear( );
}
}
void T_SyncViewImpl_::displayToolbar( ) noexcept
{
using namespace ImGui;
auto& sync( Common::Sync( ) );
if ( sync.playing( ) ) {
UI::Main( ).actionButton( "Stop" );
} else {
UI::Main( ).actionButton( "Play" );
}
SameLine( );
if ( ToolbarButton( ICON_FA_BACKWARD , BtSize , "Rewind to 00:00.000" ) ) {
sync.setTime( 0 );
}
ToolbarSeparator( );
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 ( ToolbarButton( followTime ? ICON_FA_LOCK : ICON_FA_UNLOCK , BtSize ,
followTime ? "Follows the current position.\nClick to untie."
: "Not tied to the current position.\nClick to follow." ) ) {
followTime = !followTime;
}
ToolbarSeparator( );
if ( ToolbarButton( ICON_FA_CLOCK_O , BtSize , "Change duration and time units." ) ) {
UI::Main( ).pushDialog( NewOwned< T_ChangeDurationDialog_ >(
sync.durationUnits( ) , sync.durationUnitSize( ) ) );
}
ToolbarSeparator( );
if ( ToolbarButton( ICON_FA_LINE_CHART , BtSize ,
"Select curves or sets thereof to display & edit." ) ) {
const bool displaySelector{ sub == SW_INPUT_SELECTOR
|| sub == SW_OVERRIDE_SELECTOR };
sub = displaySelector ? SW_NONE : SW_INPUT_SELECTOR;
curveFinder = T_String{};
}
}
/*------------------------------------------------------------------------------*/
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 float widgetWidth{ width - style.ScrollbarSize };
const ImRect bbHeader{
cPos ,
cPos + ImVec2( widgetWidth , SeqHeaderHeight )
};
const ImRect bbDisplay{
ImVec2{ cPos.x , bbHeader.Max.y } ,
ImVec2{ cPos.x + widgetWidth ,
GetWindowPos( ).y + ws.y
- style.FramePadding.y * 2
- style.ScrollbarSize }
};
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 , widgetWidth - 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( );
}
// Vertical scrollbar - tracks
const float totalHeight{ sCurves.size( ) * ( TrackHeight + TrackPadding * 2.f ) };
if ( vScroll > totalHeight - bbDisplay.GetHeight( ) ) {
vScroll = ImMax( 0.f , totalHeight - bbDisplay.GetHeight( ) );
}
// FIXME overrides
UserScrollbar( false , totalHeight , bbDisplay.GetHeight( ) , &vScroll ,
bbHeader.GetTR( ) , bbAll.GetHeight( ) );
// Horizontal scrollbar - time
auto& sync( Common::Sync( ) );
float rsPos = startPixel;
if ( UserScrollbar( true , totalPixels , widgetWidth , &rsPos ,
bbDisplay.GetBL( ) , bbDisplay.GetWidth( ) ) ) {
startPos = sync.durationUnits( ) * rsPos / totalPixels;
checkLockMode = true;
}
PopID( );
EndGroup( );
auto& io( GetIO( ) );
if ( hovered && ( io.MouseDown[ 0 ] || io.MouseDown[ 1 ] ) ) {
SetActiveID( seqId , win );
FocusWindow( win );
}
const bool active( GetCurrentContext( )->ActiveId == seqId );
const float mPixels{ io.MousePos.x - bbAll.Min.x + startPixel };
const float mTime{ mPixels * sync.duration( ) / totalPixels };
if ( !active ) {
if ( !hovered ) {
return;
}
if ( io.MouseWheel != 0 ) {
if ( io.KeyShift ) {
zoomLevel = ImSaturate( zoomLevel + .025 * io.MouseWheel );
} else {
vScroll = ImClamp( vScroll - 3.f * io.MouseWheel ,
0.f ,
ImMax( 0.f , totalHeight - bbDisplay.GetHeight( ) ) );
}
}
if ( bbDisplay.Contains( io.MousePos ) ) {
displayTooltips( mTime );
}
if ( ImGuiIO_MouseHorizWheel != 0 ) {
startPos = ImClamp(
sync.durationUnits( )
* ( startPixel - 20.f * ImGuiIO_MouseHorizWheel )
/ totalPixels ,
0.f , sync.durationUnits( ) );
checkLockMode = true;
}
return;
}
if ( zoomInProgress && !io.MouseDown[ 1 ] ) {
zoomInProgress = false;
checkLockMode = true;
const auto zMin{ std::min( firstZoomPixel , curZoomPixel ) } ,
zMax{ std::max( firstZoomPixel , curZoomPixel ) } ,
diff{ zMax - zMin };
if ( diff > 4 ) {
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;
}
}
} else if ( zoomInProgress && io.MouseDown[ 1 ] ) {
curZoomPixel = mPixels;
return;
}
if ( selPointDnD && handlePointDrag( mPixels , io.MouseDown[ 0 ] ) ) {
return;
}
if ( !( io.MouseDown[ 0 ] || io.MouseDown[ 1 ] ) ) {
ClearActiveID( );
return;
}
const auto mp{ getMousePos( ) };
if ( mp.type == E_MousePosType::NONE ) {
if ( io.MouseDown[ 0 ] ) {
sync.setTime( mTime );
}
if ( io.MouseDown[ 1 ] ) {
firstZoomPixel = mPixels;
zoomInProgress = true;
curZoomPixel = mPixels;
}
} else if ( mp.type == E_MousePosType::TRACK ) {
auto const& dTrack{ dspTracks[ mp.index ] };
if ( io.MouseDown[ 0 ] || io.MouseDown[ 1 ] ) {
selId = dTrack.id;
selIsOverride = dTrack.isOverride;
selSegment = decltype( selSegment ){};
selPoint = decltype( selPoint ){};
sub = E_SubWindow::SW_TRACK;
}
} else if ( mp.type == E_MousePosType::SEGMENT ) {
auto const& dSeg{ dspSegments[ mp.index ] };
auto const& dTrack{ dspTracks[ dSeg.track ] };
if ( io.MouseDown[ 0 ] || io.MouseDown[ 1 ] ) {
selId = dTrack.id;
selIsOverride = dTrack.isOverride;
selSegment = dSeg.seg;
selPoint = decltype( selPoint ){};
sub = E_SubWindow::SW_SEGMENT;
}
} else if ( mp.type == E_MousePosType::POINT ) {
auto const& dPoint{ dspPoints[ mp.index ] };
auto const& dSeg{ dspSegments[ dPoint.seg ] };
auto const& dTrack{ dspTracks[ dSeg.track ] };
if ( io.MouseDown[ 0 ] || io.MouseDown[ 1 ] ) {
selId = dTrack.id;
selIsOverride = dTrack.isOverride;
selSegment = dSeg.seg;
selPoint = dPoint.index;
selPointDnD = io.MouseDown[ 0 ] && dPoint.index != 0;
if ( selPointDnD ) {
assert( selUpdate == E_ChangeType::NONE );
selPointDnDStart = selPointDnDCur = mPixels;
selUpdatingOriginal = *sync.getCurve( dTrack.id ); // XXX
selUpdate = E_ChangeType::POINT_DND;
}
sub = E_SubWindow::SW_POINT;
}
}
}
void T_SyncViewImpl_::computeMetrics(
const float innerWidth ) noexcept
{
auto& sync( Common::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 ( checkLockMode ) {
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 , units * ( totalPixels - innerWidth ) / totalPixels );
}
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 );
checkLockMode = 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( UI::Main( ).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 };
TimeToString_( buffer , sizeof( buffer ) , time );
RenderTextClipped( taStart , taEnd , buffer , nullptr , nullptr ,
ImVec2{ .5f , .05f + ( ( bar % 2 ) ? .9f : 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 ++;
}
// Display the curve / override controls
dspTracks.clear( );
dspSegments.clear( );
dspPoints.clear( );
if ( sCurves.size( ) != 0 ) {
ImRect subBb{ inner };
subBb.Min.y += TrackPadding - vScroll;
subBb.Max.y = subBb.Min.y + TrackHeight;
float hue{ 0.12f };
// TODO: display overrides
sequencerTracks( hue , subBb , inner );
}
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_::sequencerTracks(
float& hue ,
ImRect& subBb ,
ImRect const& container ) noexcept
{
auto& sync{ Common::Sync( ) };
const auto nc{ sCurves.size( ) };
for ( auto i = 0u ; i < nc ; i ++ ) {
if ( sCurves.values( )[ i ] ) {
continue;
}
auto const& name{ sCurves.keys( )[ i ] };
auto* const curve{ sync.getCurve( name ) };
sequencerTrack( hue , subBb , container , name , curve );
subBb.Min.y += TrackHeight + 2 * TrackPadding;
subBb.Max.y += TrackHeight + 2 * TrackPadding;
hue = fmodf( hue + .17f , 1.f );
}
}
void T_SyncViewImpl_::sequencerTrack(
float& hue ,
ImRect const& bb ,
ImRect const& container ,
T_String const& id ,
T_SyncCurve const* curve ) noexcept
{
// Don't display if the track is fully hidden
if ( !container.Overlaps( bb ) ) {
return;
}
// Add track display record
const auto dTrackIdx{ dspTracks.size( ) };
auto& dTrack{ dspTracks.addNew( ) };
dTrack.id = id;
dTrack.isOverride = false;
dTrack.dispSegs = 0;
dTrack.firstSeg = dspSegments.size( );
dTrack.area = bb;
// Compute colors
using namespace ImGui;
const bool sCurve{ selId && !selIsOverride && selId == id };
const float scv{ sCurve ? 1.f : .7f };
const auto bgColor{ ColorHSVAToU32( hue , .25f , scv , .25f ) } ,
borderColor{ ColorHSVAToU32( hue , .5f , scv , 1.f ) };
const uint32_t segColors[] = {
ColorHSVAToU32( hue - .03f , .4f , .7f , 1.f ) ,
ColorHSVAToU32( hue + .03f , .4f , .7f , 1.f ) ,
ColorHSVAToU32( hue - .03f , .4f , 1.f , 1.f ) ,
ColorHSVAToU32( hue + .03f , .4f , 1.f , 1.f ) ,
};
// Draw the bar itself
const ImRect bar{
ImVec2{ ImFloor( bb.Min.x ) ,
ImFloor( ImMax( container.Min.y , bb.Min.y ) ) } ,
ImVec2{ ImFloor( bb.Max.x ) - 1 ,
ImFloor( ImMin( container.Max.y , bb.Max.y ) ) }
};
auto* const dl{ GetWindowDrawList( ) };
dl->AddRectFilled( bar.Min , bar.Max , bgColor );
if ( container.Contains( bb.GetTL( ) ) ) {
dl->AddLine( bar.GetTL( ) , bar.GetTR( ) , borderColor );
}
if ( container.Contains( bb.GetBL( ) ) ) {
dl->AddLine( bar.GetBL( ) , bar.GetBR( ) , borderColor );
}
// Left only has a border if this is the start
if ( startPos == 0.f ) {
dl->AddLine( bar.GetTL( ) , bar.GetBL( ) , borderColor );
}
// Right has a border if the end is being displayed.
if ( startPixel + container.GetWidth( ) >= totalPixels ) {
dl->AddLine( bar.GetTR( ) , bar.GetBR( ) , borderColor );
}
dl->PushClipRect( bar.Min , bar.Max );
// If there's a curve, go through all segments
const auto units{ Common::Sync( ).durationUnits( ) };
const bool useCopy{ sCurve && curve && selUpdatingCopy };
const auto nSeg{ curve
? ( useCopy ? *selUpdatingCopy : *curve ).segments.size( )
: 0u };
uint32_t segStart{ 0 };
for ( auto i = 0u ; i < nSeg ; i ++ ) {
auto const& seg{ ( useCopy ? *selUpdatingCopy : *curve ).segments[ i ] };
const auto segDur{ [&](){
auto t{ 0u };
for ( auto d : seg.durations ) {
t += d;
}
return t;
}() };
const float xStart{ ImFloor( container.Min.x + ( segStart - startPos ) * totalPixels / units ) };
const float xEnd{ xStart + ImFloor( segDur * totalPixels / units ) };
const ImRect segFull{
ImVec2{ xStart , bar.Min.y + 1 } ,
ImVec2{ xEnd , bar.Max.y }
};
segStart += segDur;
if ( !segFull.Overlaps( bar ) ) {
continue;
}
// Add segment to displayed list
const bool sSegment{ sCurve && selSegment && *selSegment == i };
const auto color{ segColors[ i % 2 + ( sSegment ? 2 : 0 ) ] };
auto dSegIdx{ dspSegments.size( ) };
auto& dSeg{ dspSegments.addNew( ) };
dSeg.area = segFull;
dSeg.track = dTrackIdx;
dSeg.seg = i;
dSeg.dispPoints = 0;
dSeg.firstPoint = dspPoints.size( );
dTrack.dispSegs ++;
// Draw segment
const ImRect segBar{
ImVec2{ ImMax( bar.Min.x , segFull.Min.x ) , segFull.Min.y } ,
ImVec2{ ImMin( bar.Max.x , segFull.Max.x ) , segFull.Max.y }
};
dl->AddRectFilled( segBar.Min , segBar.Max , color );
dl->PushClipRect( segBar.Min , segBar.Max );
// Handle points
const auto nd{ seg.durations.size( ) };
const auto ym{ bb.Min.y + PointRadius + 1 };
auto cDur{ 0 };
for ( auto j = 0u ; j < nd + 1 ; j ++ ) {
const ImVec2 ctr{
std::roundf( xStart + cDur * totalPixels / units ) ,
ym
};
const bool sPoint{ sSegment && selPoint && *selPoint == j };
dl->AddCircleFilled( ctr , PointRadius ,
sPoint ? ColPointSelected : ColPointNormal );
cDur += j < nd ? seg.durations[ j ] : 0;
// Add point record
auto& dPoint{ dspPoints.addNew( ) };
dPoint.center = ctr;
dPoint.index = j;
dPoint.mode =
j == 0 ? T_TrackPointDisplay::START : (
j == nd ? T_TrackPointDisplay::END :
T_TrackPointDisplay::MIDDLE );
dPoint.seg = dSegIdx;
dSeg.dispPoints ++;
}
dl->PopClipRect( );
}
dl->PopClipRect( );
}
/*----------------------------------------------------------------------------*/
void T_SyncViewImpl_::displayTooltips(
const float time ) noexcept
{
auto mp{ getMousePos( ) };
if ( mp.type == E_MousePosType::NONE ) {
return;
}
// Track / segment / point from mouse info
auto const& track( [&](){
if ( mp.type == E_MousePosType::POINT ) {
return dspTracks[ dspSegments[
dspPoints[ mp.index ].seg ].track ];
}
if ( mp.type == E_MousePosType::SEGMENT ) {
return dspTracks[ dspSegments[ mp.index ].track ];
}
return dspTracks[ mp.index ];
}() );
auto const* const seg( [&]() -> T_TrackSegDisplay const* {
if ( mp.type == E_MousePosType::TRACK ) {
return nullptr;
}
if ( mp.type == E_MousePosType::SEGMENT ) {
return &dspSegments[ mp.index ];
}
return &dspSegments[ dspPoints[ mp.index ].seg ];
}() );
auto const* const point( [&]() -> T_TrackPointDisplay const* {
if ( mp.type != E_MousePosType::POINT ) {
return nullptr;
}
return &dspPoints[ mp.index ];
}() );
// Curve from track
auto& sync{ Common::Sync( ) };
T_SyncCurve const* const curve{ sync.getCurve( track.id ) };
assert( mp.type == E_MousePosType::TRACK || curve != nullptr );
// Time offset
const float dTime( [&](){
if ( point ) {
auto tDur{ .0f };
for ( auto i = 0u ; i <= seg->seg ; i ++ ) {
auto const& s{ curve->segments[ i ] };
auto const nd{ i == seg->seg
? point->index
: s.durations.size( ) };
for ( auto j = 0u ; j < nd ; j ++ ) {
tDur += s.durations[ j ];
}
}
return tDur * sync.durationUnitSize( );
}
return time;
}() );
const float dUTime{ dTime / sync.durationUnitSize( ) };
char buffer[ 12 ];
TimeToString_( buffer , sizeof( buffer ) , dTime );
const float value{ point
? curve->segments[ seg->seg ].values[ point->index ]
: ( curve ? curve->computeValue( dUTime )
: sync.inputs( )[ sync.inputPos( track.id ) ] ) };
stringBuffer.clear( ) << track.id << " (input)\n";
if ( mp.type == E_MousePosType::TRACK ) {
stringBuffer << "No segment";
} else {
auto const& s{ curve->segments[ seg->seg ] };
if ( mp.type == E_MousePosType::SEGMENT ) {
stringBuffer << "Segment type: " << s.type;
} else {
stringBuffer << "On " << s.type << " segment, index " << point->index;
}
}
stringBuffer << "\nTime: " << buffer << "\nValue: " << value;
using namespace ImGui;
stringBuffer << '\0';
BeginTooltip( );
Text( "%s" , stringBuffer.data( ) );
EndTooltip( );
}
bool T_SyncViewImpl_::handlePointDrag(
const float mPixels ,
bool mouseDown ) noexcept
{
auto& sync( Common::Sync( ) );
selPointDnDCur = mPixels;
const float diff{ selPointDnDCur - selPointDnDStart };
const int32_t diffUnits{ int32_t( round( diff * sync.durationUnits( ) / totalPixels ) ) };
const bool moved{ fabsf( diff ) >= 2 && abs( diffUnits ) > 0 };
// Update the point as necessary
if ( moved ) {
selUpdatingCopy = selUpdatingOriginal; // XXX
auto& seg{ selUpdatingCopy->segments[ *selSegment ] };
if ( *selPoint == seg.durations.size( ) ) {
// We're dragging the end point
// XXX make it work "normally"
seg.durations.last( ) = std::max( 1 ,
diffUnits + int32_t( seg.durations.last( ) ) );
} else {
// We're dragging some other point, move units
// from one side to the other
assert( *selPoint > 0 );
auto& d0{ seg.durations[ *selPoint - 1 ] };
auto& d1{ seg.durations[ *selPoint ] };
const int32_t mmNeg( 1 - d0 ) , mmPos( d1 - 1 );
const int32_t diff{ diffUnits < mmNeg ? mmNeg
: ( diffUnits > mmPos ? mmPos : diffUnits ) };
d0 += diff;
d1 -= diff;
}
sync.setCurve( *selUpdatingCopy );
} else {
selUpdatingCopy.clear( );
}
if ( mouseDown ) {
return true;
}
assert( selUpdate == E_ChangeType::POINT_DND );
selUpdate = E_ChangeType::NONE;
selPointDnD = false;
sync.setCurve( std::move( *selUpdatingOriginal ) );
selUpdatingOriginal.clear( );
if ( moved ) {
SyncEditor::ReplaceCurve( std::move( *selUpdatingCopy ) );
selUpdatingCopy.clear( );
}
return false;
}
/*----------------------------------------------------------------------------*/
T_SyncViewImpl_::T_MousePos T_SyncViewImpl_::getMousePos( ) const noexcept
{
auto const& mp{ ImGui::GetIO( ).MousePos };
for ( auto i = 0u ; i < dspTracks.size( ) ; i ++ ) {
auto const& track{ dspTracks[ i ] };
if ( !track.area.Contains( mp ) ) {
continue;
}
for ( auto j = 0u ; j < track.dispSegs ; j ++ ) {
auto const& seg{ dspSegments[ track.firstSeg + j ] };
if ( !seg.area.Contains( mp ) ) {
continue;
}
for ( auto k = 0u ; k < seg.dispPoints ; k ++ ) {
auto const& p{ dspPoints[ seg.firstPoint + k ] };
if ( ImLengthSqr( mp - p.center ) <= PointRadiusSqr ) {
return T_MousePos{ E_MousePosType::POINT ,
seg.firstPoint + k };
}
}
return T_MousePos{ E_MousePosType::SEGMENT ,
track.firstSeg + j };
}
return T_MousePos{ E_MousePosType::TRACK , i };
}
return T_MousePos{ E_MousePosType::NONE , 0 };
}
/*----------------------------------------------------------------------------*/
void T_SyncViewImpl_::displayTrackSelectorWindow( ) 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_INPUT_SELECTOR , buttonWidth ) ) {
sub = SW_INPUT_SELECTOR;
}
SameLine( 0 );
if ( FakeTab_( "Overrides" , sub == SW_OVERRIDE_SELECTOR , buttonWidth ) ) {
sub = SW_OVERRIDE_SELECTOR;
}
// Content
switch ( sub ) {
case SW_INPUT_SELECTOR:
displayInputSelector( );
break;
case SW_OVERRIDE_SELECTOR:
displayOverrideSelector( );
break;
default:
fprintf( stderr , "unexpected bullshit in sync view\n" );
std::abort( );
}
End( );
}
void T_SyncViewImpl_::displayInputSelector( ) noexcept
{
using namespace ImGui;
T_Array< T_String > names{ Common::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 ) {
PushDisabled( );
}
bool select{ present };
stringBuffer.clear( ) << n << '\0';
if ( Checkbox( stringBuffer.data( ) , &select ) ) {
if ( select ) {
sCurves.add( n , false );
} else {
sCurves.remove( n );
}
}
if ( overriden ) {
PopDisabled( );
}
}
EndChild( );
}
void T_SyncViewImpl_::displayOverrideSelector( ) noexcept
{
using namespace ImGui;
/*
* An override can be selected directly if the inputs that are part of
* it are consistent.
*
* A pair of inputs are consistent if
* - one or both inputs do not have curve information attached;
* - the durations and segment boundaries of the shortest curve
* attached to the inputs match the boundaries and durations from the
* other input's curve OR the last segment is shorter but its end
* matches the location of one of the points in the corresponding
* segment of the other curve.
*
* If the inputs are not consistent, it will be necessary to reset
* some of their curves; best option would be to try and be clever, but
* a temporary measure (resetting all curves to match the structure of
* the first longest curve) would work.
*/
BeginChild( "content" );
Common::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( );
return true;
}
return TreeNodeEx( &sos.cTitle[ 0 ] ,
ImGuiTreeNodeFlags_DefaultOpen );
}
if ( ! exit ) {
return false;
}
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;
}() };
// FIXME MOVE THIS \/ //
bool bad{ false };
for ( auto i = 0u ; i < in.size( ) - 1 && !bad ; i ++ ) {
T_SyncCurve const* const c0{
Common::Sync( ).getCurve( in[ i ] )
};
if ( !c0 ) {
continue;
}
for ( auto j = i + 1 ; j < in.size( ) && !bad ; j ++ ) {
T_SyncCurve const* const c1{
Common::Sync( ).getCurve( in[ j ] )
};
if ( !c1 ) {
continue;
}
const auto res{ c0->matches( *c1 ) };
bad = ( res == E_SyncCurveMatch::MISMATCH );
}
}
// FIXME MOVE THIS /\ //
if ( hasCurves ) {
PushDisabled( );
}
if ( bad ) {
PushStyleColor( ImGuiCol_Text , 0xff0000ff );
}
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 ( bad ) {
PopStyleColor( );
}
if ( hasCurves ) {
PopDisabled( );
}
return true;
} );
EndChild( );
}
/*----------------------------------------------------------------------------*/
void T_SyncViewImpl_::displayTrackWindow( ) 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( "Selected track" , &displayed , ImGuiWindowFlags_NoCollapse );
if ( !displayed ) {
End( );
sub = SW_NONE;
return;
}
// Get the curve
auto& sync{ Common::Sync( ) };
auto const* const curve{ sync.getCurve( selId ) };
Text( "Curve:" );
SameLine( 110 );
Text( "%s" , selId.toOSString( ).data( ) );
if ( !sync.hasInput( selId ) ) {
Text( " " );
SameLine( 110 );
Text( "No matching input" );
}
Text( "Segments:" );
SameLine( 110 );
Text( "%d" , curve ? curve->segments.size( ) : 0 );
Separator( );
// Compute total duration
const uint32_t duration{ [&](){
uint32_t t{ 0 };
if ( !curve ) {
return 0u;
}
for ( auto const& s : curve->segments ) {
for ( auto const& d : s.durations ) {
t += d;
}
}
return t;
}() };
const float tDuration{ duration * sync.durationUnitSize( ) };
Text( "Duration:" );
SameLine( 110 );
Text( "%d units" , duration );
Text( " " );
SameLine( 110 );
Text( "%.3f seconds" , tDuration );
const float dDuration{ sync.duration( ) };
if ( tDuration == 0.f ) {
Text( " " );
SameLine( 110 );
Text( "Empty track" );
} else if ( tDuration == dDuration ) {
Text( " " );
SameLine( 110 );
Text( "Covers the whole demo" );
} else if ( tDuration > dDuration ) {
Text( " " );
SameLine( 110 );
Text( "Longer than the demo" );
}
if ( ( curve && !curve->segments.empty( ) ) || tDuration < dDuration ) {
Separator( );
}
if ( curve && !curve->segments.empty( ) ) {
Text( " " );
SameLine( 110 );
if ( Button( "Clear" , ImVec2{ -1 , 0 } ) ) {
SyncEditor::DeleteCurve( selId );
}
}
if ( tDuration < dDuration ) {
Text( " " );
SameLine( 110 );
if ( Button( "Append segment" , ImVec2{ -1 , 0 } ) ) {
const uint32_t ns{ std::max( 1u ,
( sync.durationUnits( ) - duration ) / 2 ) };
SyncEditor::AppendSegment( selId , ns );
}
}
End( );
}
void T_SyncViewImpl_::displaySegmentWindow( ) 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( "Selected segment" , &displayed , ImGuiWindowFlags_NoCollapse );
if ( !displayed ) {
End( );
sub = SW_NONE;
return;
}
const uint32_t sid{ *selSegment };
Text( "Curve:" );
SameLine( 110 );
Text( "%s" , selId.toOSString( ).data( ) );
Text( "Index:" );
SameLine( 110 );
Text( "%d" , sid );
Separator( );
// Access curve and segment
auto& sync{ Common::Sync( ) };
auto const* const curve{ sync.getCurve( selId ) };
auto const& segment{ curve->segments[ sid ] };
// Find start and duration
uint32_t start{ 0 } , duration{ 0 };
for ( auto i = 0u ; i <= sid ; i ++ ) {
auto const& s{ curve->segments[ i ] };
auto& tgt{ i == sid ? duration : start };
for ( auto d : s.durations ) {
tgt += d;
}
}
const float tStart{ sync.durationUnitSize( ) * start } ,
tDuration{ sync.durationUnitSize( ) * duration };
Text( "Start:" );
SameLine( 110 );
Text( "%d units" , start );
Text( " " );
SameLine( 110 );
Text( "%.3f seconds" , tStart );
Text( "End:" );
SameLine( 110 );
Text( "%d units" , start + duration );
Text( " " );
SameLine( 110 );
Text( "%.3f seconds" , tStart + tDuration );
Text( "Duration:" );
SameLine( 110 );
Text( "%d units" , duration );
Text( " " );
SameLine( 110 );
Text( "%.3f seconds" , tDuration );
Separator( );
// Generate the combo box's data
static constexpr T_SyncSegment::E_SegmentType types[] = {
T_SyncSegment::LINEAR , T_SyncSegment::RAMP , T_SyncSegment::SMOOTH
};
stringBuffer.clear( );
for ( auto t : types ) {
stringBuffer << t << '\0';
}
stringBuffer << '\0';
Text( "Type:" );
SameLine( 110 );
PushItemWidth( -1 );
int t{ int( segment.type ) };
const bool change{ Combo( "##type" , &t , stringBuffer.data( ) ) };
PopItemWidth( );
Separator( );
Text( "Points:" );
SameLine( 110 );
Text( "%d" , segment.values.size( ) );
// Find min/max
float sMin{ FLT_MAX } , sMax{ -FLT_MAX };
for ( auto v : segment.values ) {
sMin = ImMin( sMin , v );
sMax = ImMax( sMax , v );
}
Text( "Min. value:" );
SameLine( 110 );
Text( "%f" , sMin );
Text( "Max. value:" );
SameLine( 110 );
Text( "%f" , sMax );
Separator( );
Text( " " );
SameLine( 110 );
if ( Button( "Delete segment" , ImVec2{ -1 , 0 } ) ) {
if ( curve->segments.size( ) > 1 ) {
selSegment = ( sid == 0 ? 0 : ( sid - 1 ) );
} else {
selSegment = decltype( selSegment ){};
}
SyncEditor::DeleteSegment( selId , sid );
}
if ( change ) {
SyncEditor::SetSegmentType( *curve , sid ,
T_SyncSegment::E_SegmentType( t ) );
}
End( );
}
void T_SyncViewImpl_::displayPointWindow( ) 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( "Selected point" , &displayed , ImGuiWindowFlags_NoCollapse );
if ( !displayed ) {
End( );
sub = SW_NONE;
return;
}
// Access curve, segment and point
const uint32_t sid{ *selSegment };
const uint32_t pid{ *selPoint };
auto& sync{ Common::Sync( ) };
auto const* const curve{ selUpdatingCopy
? selUpdatingCopy.target( )
: sync.getCurve( selId ) };
auto const& segment{ curve->segments[ sid ] };
Text( "Curve:" );
SameLine( 110 );
Text( "%s" , selId.toOSString( ).data( ) );
Text( "Segment index:" );
SameLine( 110 );
Text( "%d" , sid );
Text( "Point index:" );
SameLine( 110 );
Text( "%d / %d" , pid , segment.durations.size( ) );
Separator( );
// Find start and duration
uint32_t segStart{ 0 } , pointPosRel{ 0 };
for ( auto i = 0u ; i <= sid ; i ++ ) {
auto const& s{ curve->segments[ i ] };
auto& tgt{ i == sid ? pointPosRel : segStart };
const auto end{ i == sid ? pid : s.durations.size( ) };
for ( auto j = 0u ; j < end ; j ++ ) {
tgt += s.durations[ j ];
}
}
const uint32_t pointPosAbs{ segStart + pointPosRel };
const float tPointPosAbs{ sync.durationUnitSize( ) * pointPosAbs } ,
tPointPosRel{ sync.durationUnitSize( ) * pointPosRel };
Text( "Absolute:" );
SameLine( 110 );
Text( "%d units" , pointPosAbs );
Text( " " );
SameLine( 110 );
Text( "%.3f seconds" , tPointPosAbs );
Text( "Relative:" );
SameLine( 110 );
Text( "%d units" , pointPosRel );
Text( " " );
SameLine( 110 );
Text( "%.3f seconds" , tPointPosRel );
Separator( );
Text( "Value:" );
SameLine( 110 );
float value{ segment.values[ pid ] };
PushItemWidth( -1 );
DragFloat( "##value" , &value , .01f , 0 , 0 , "%.6f" );
const bool changed{ IsItemActive( ) };
PopItemWidth( );
const bool canUseButtons{ !changed && selUpdate == E_ChangeType::NONE };
const auto canInsertBefore{ pid != 0
&& segment.durations[ pid - 1 ] > 1 };
const auto canInsertAfter{ pid != segment.durations.size( )
&& segment.durations[ pid ] > 1 };
if ( pid != 0 && pid != segment.durations.size( ) ) {
Separator( );
Text( " " );
SameLine( 110 );
if ( Button( "Delete point" , ImVec2{ -1 , 0 } ) && canUseButtons ) {
SyncEditor::DeletePoint( selId , sid , pid );
selPoint.clear( );
sub = SW_SEGMENT;
}
}
if ( canInsertAfter || canInsertBefore ) {
Separator( );
if ( canInsertBefore ) {
Text( " " );
SameLine( 110 );
if ( Button( "Insert before" , ImVec2{ -1 , 0 } ) && canUseButtons ) {
SyncEditor::InsertPoint( selId , sid , pid );
(*selPoint) ++;
}
}
if ( canInsertAfter ) {
Text( " " );
SameLine( 110 );
if ( Button( "Insert after" , ImVec2{ -1 , 0 } ) && canUseButtons ) {
SyncEditor::InsertPoint( selId , sid , pid + 1 );
}
}
}
if ( changed ) {
if ( selUpdate == E_ChangeType::NONE ) {
selUpdatingOriginal = *curve;
selUpdatingCopy = *curve;
selUpdate = E_ChangeType::POINT_VALUE;
} else {
assert( selUpdate == E_ChangeType::POINT_VALUE );
}
selUpdatingCopy->segments[ sid ].values[ pid ] = value;
sync.setCurve( *selUpdatingCopy );
} else if ( selUpdate == E_ChangeType::POINT_VALUE ) {
selUpdate = E_ChangeType::NONE;
sync.setCurve( *selUpdatingOriginal );
SyncEditor::ReplaceCurve( std::move( *selUpdatingCopy ) );
selUpdatingCopy.clear( );
}
End( );
}
} // namespace <anon>
/*= T_SyncView ===============================================================*/
T_SyncView::T_SyncView( ) noexcept
: A_PrivateImplementation( new T_SyncViewImpl_( ) )
{ }
bool T_SyncView::display( ) noexcept
{
return p< T_SyncViewImpl_ >( ).display( );
}