diff --git a/profiler/build/win32/Tracy.vcxproj b/profiler/build/win32/Tracy.vcxproj
index ec13eba9..a45e987f 100644
--- a/profiler/build/win32/Tracy.vcxproj
+++ b/profiler/build/win32/Tracy.vcxproj
@@ -137,6 +137,7 @@
+
@@ -273,6 +274,7 @@
+
diff --git a/profiler/build/win32/Tracy.vcxproj.filters b/profiler/build/win32/Tracy.vcxproj.filters
index c233fda0..a0f18cf7 100644
--- a/profiler/build/win32/Tracy.vcxproj.filters
+++ b/profiler/build/win32/Tracy.vcxproj.filters
@@ -357,6 +357,9 @@
server
+
+ server
+
@@ -725,6 +728,9 @@
common
+
+ server
+
diff --git a/server/TracyTimelineItem.cpp b/server/TracyTimelineItem.cpp
new file mode 100644
index 00000000..38fd2d41
--- /dev/null
+++ b/server/TracyTimelineItem.cpp
@@ -0,0 +1,156 @@
+#include
+
+#include "TracyImGui.hpp"
+#include "TracyMouse.hpp"
+#include "TracyTimelineItem.hpp"
+#include "TracyView.hpp"
+
+namespace tracy
+{
+
+TimelineItem::TimelineItem( View& view, const Worker& worker )
+ : m_visible( true )
+ , m_showFull( true )
+ , m_height( 0 )
+ , m_offset( 0 )
+ , m_view( view )
+ , m_worker( worker )
+{
+}
+
+void TimelineItem::Draw( bool firstFrame, double pxns, int& offset, const ImVec2& wpos, bool hover, float yMin, float yMax )
+{
+ if( !m_visible )
+ {
+ m_height = 0;
+ m_offset = 0;
+ return;
+ }
+ if( IsEmpty() ) return;
+
+ const auto w = ImGui::GetContentRegionAvail().x - 1;
+ const auto ty = ImGui::GetTextLineHeight();
+ const auto yPos = AdjustThreadPosition( wpos.y, offset );
+ const auto dpos = wpos + ImVec2( 0.5f, 0.5f );
+ const auto oldOffset = offset;
+ auto draw = ImGui::GetWindowDrawList();
+
+ ImGui::PushID( this );
+ ImGui::PushClipRect( wpos + ImVec2( 0, offset ), wpos + ImVec2( w, offset + m_height ), true );
+
+ float labelWidth;
+ const bool drawHeader = yPos + ty >= yMin && yPos <= yMax;
+ if( drawHeader )
+ {
+ const auto to = 9.f;
+ const auto th = ( ty - to ) * sqrt( 3 ) * 0.5;
+
+ const auto color = HeaderColor();
+ const auto colorInactive = HeaderColorInactive();
+
+ if( m_showFull )
+ {
+ draw->AddTriangleFilled( wpos + ImVec2( to/2, offset + to/2 ), wpos + ImVec2( ty - to/2, offset + to/2 ), wpos + ImVec2( ty * 0.5, offset + to/2 + th ), color );
+ }
+ else
+ {
+ draw->AddTriangle( wpos + ImVec2( to/2, offset + to/2 ), wpos + ImVec2( to/2, offset + ty - to/2 ), wpos + ImVec2( to/2 + th, offset + ty * 0.5 ), colorInactive, 2.0f );
+ }
+ const auto label = HeaderLabel();
+ labelWidth = ImGui::CalcTextSize( label ).x;
+ DrawTextContrast( draw, wpos + ImVec2( ty, offset ), m_showFull ? color : colorInactive, label );
+ DrawLine( draw, dpos + ImVec2( 0, offset + ty - 1 ), dpos + ImVec2( w, offset + ty - 1 ), 0x8844DDDD );
+
+ if( hover && ImGui::IsMouseHoveringRect( wpos + ImVec2( 0, offset ), wpos + ImVec2( ty + labelWidth, offset + ty ) ) )
+ {
+ HeaderTooltip( label );
+
+ if( IsMouseClicked( 0 ) )
+ {
+ m_showFull = !m_showFull;
+ }
+ if( IsMouseClicked( 2 ) )
+ {
+ m_view.ZoomToRange( RangeBegin(), RangeEnd() );
+ }
+ if( IsMouseClicked( 1 ) )
+ {
+ ImGui::OpenPopup( "menuPopup" );
+ }
+ }
+ }
+
+ if( ImGui::BeginPopup( "menuPopup" ) )
+ {
+ if( ImGui::MenuItem( ICON_FA_EYE_SLASH " Hide" ) )
+ {
+ m_visible = false;
+ ImGui::CloseCurrentPopup();
+ }
+ ImGui::EndPopup();
+ }
+
+ auto hdrOffset = offset;
+ offset += ty;
+ if( m_showFull )
+ {
+ DrawContents( pxns, offset, wpos, hover, yMin, yMax );
+ if( drawHeader )
+ {
+ HeaderExtraContents( hdrOffset, wpos, labelWidth );
+ }
+ }
+
+ offset += 0.2 * ty;
+ AdjustThreadHeight( firstFrame, oldOffset, offset );
+
+ ImGui::PopClipRect();
+ ImGui::PopID();
+}
+
+void TimelineItem::AdjustThreadHeight( bool firstFrame, int oldOffset, int& offset )
+{
+ const auto h = offset - oldOffset;
+ if( m_height > h )
+ {
+ m_height = h;
+ offset = oldOffset + m_height;
+ }
+ else if( m_height < h )
+ {
+ if( firstFrame )
+ {
+ m_height = h;
+ offset = oldOffset + h;
+ }
+ else
+ {
+ const auto diff = h - m_height;
+ const auto move = std::max( 2.0, diff * 10.0 * ImGui::GetIO().DeltaTime );
+ m_height = int( std::min( m_height + move, h ) );
+ offset = oldOffset + m_height;
+ }
+ }
+}
+
+float TimelineItem::AdjustThreadPosition( float wy, int& offset )
+{
+ if( m_offset < offset )
+ {
+ m_offset = offset;
+ }
+ else if( m_offset > offset )
+ {
+ const auto diff = m_offset - offset;
+ const auto move = std::max( 2.0, diff * 10.0 * ImGui::GetIO().DeltaTime );
+ offset = m_offset = int( std::max( m_offset - move, offset ) );
+ }
+ return offset + wy;
+}
+
+void TimelineItem::VisibilityCheckbox()
+{
+ SmallCheckbox( HeaderLabel(), &m_visible );
+}
+
+}
diff --git a/server/TracyTimelineItem.hpp b/server/TracyTimelineItem.hpp
new file mode 100644
index 00000000..ef9d7a4e
--- /dev/null
+++ b/server/TracyTimelineItem.hpp
@@ -0,0 +1,55 @@
+#ifndef __TRACYTIMELINEITEM_HPP__
+#define __TRACYTIMELINEITEM_HPP__
+
+#include
+
+#include "imgui.h"
+
+namespace tracy
+{
+
+class View;
+class Worker;
+
+class TimelineItem
+{
+public:
+ TimelineItem( View& view, const Worker& worker );
+ virtual ~TimelineItem() = default;
+
+ void Draw( bool firstFrame, double pxns, int& offset, const ImVec2& wpos, bool hover, float yMin, float yMax );
+ virtual void DrawContents( double pxns, int& offset, const ImVec2& wpos, bool hover, float yMin, float yMax ) = 0;
+
+ virtual bool IsEmpty() const { return false; }
+
+ void VisibilityCheckbox();
+
+protected:
+ virtual uint32_t HeaderColor() const = 0;
+ virtual uint32_t HeaderColorInactive() const = 0;
+ virtual uint32_t HeaderLineColor() const = 0;
+ virtual const char* HeaderLabel() const = 0;
+
+ virtual void HeaderTooltip( const char* label ) const {};
+ virtual void HeaderExtraContents( int offset, const ImVec2& wpos, float labelWidth ) {};
+
+ virtual int64_t RangeBegin() const = 0;
+ virtual int64_t RangeEnd() const = 0;
+
+private:
+ void AdjustThreadHeight( bool firstFrame, int oldOffset, int& offset );
+ float AdjustThreadPosition( float wy, int& offset );
+
+ bool m_visible;
+ bool m_showFull;
+ int m_height;
+ int m_offset;
+
+protected:
+ View& m_view;
+ const Worker& m_worker;
+};
+
+}
+
+#endif