Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render revision graph in the background #11719

Merged
merged 5 commits into from
May 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 0 additions & 4 deletions GitUI/UserControls/RevisionGrid/Columns/ColumnProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,6 @@ public virtual void OnColumnWidthChanged(DataGridViewColumnEventArgs e)
{
}

public virtual void OnVisibleRowsChanged(in VisibleRowRange visibleRowRange)
{
}

/// <summary>Attempts to get custom tool tip text for a cell in this column.</summary>
/// <remarks>Returning <c>false</c> here will not stop a tool tip being automatically displayed for truncated text.</remarks>
public virtual bool TryGetToolTip(DataGridViewCellMouseEventArgs e, GitRevision revision, [NotNullWhen(returnValue: true)] out string? toolTip)
Expand Down
242 changes: 120 additions & 122 deletions GitUI/UserControls/RevisionGrid/Columns/RevisionGraphColumnProvider.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Drawing.Drawing2D;
using GitCommands;
using GitUI.NBugReports;
using GitUI.UserControls.RevisionGrid.Graph;
using GitUI.UserControls.RevisionGrid.Graph.Rendering;
using GitUIPluginInterfaces;
using Microsoft;

namespace GitUI.UserControls.RevisionGrid.Columns
{
internal sealed class RevisionGraphColumnProvider : ColumnProvider
{
private readonly LaneInfoProvider _laneInfoProvider;
private readonly RevisionGraph _revisionGraph;
private readonly GraphCache _graphCache = new();
private readonly GraphCache _graphDisplayCache = new();
private readonly GraphCache _graphRenderCache = new();

private int _columnWidth = 0;

public RevisionGraphColumnProvider(RevisionGraph revisionGraph, IGitRevisionSummaryBuilder gitRevisionSummaryBuilder)
: base("Graph")
Expand All @@ -36,35 +37,25 @@ public RevisionGraphColumnProvider(RevisionGraph revisionGraph, IGitRevisionSumm

public override void OnCellPainting(DataGridViewCellPaintingEventArgs e, GitRevision revision, int rowHeight, in CellStyle style)
{
e.Handled = false;
if (AppSettings.ShowRevisionGridGraphColumn
&& e.State.HasFlag(DataGridViewElementStates.Visible)
&& e.RowIndex >= 0
&& e.RowIndex < _revisionGraph.Count)
try
{
try
{
if (PaintGraphCell(e.RowIndex, rowHeight, e.CellBounds, e.Graphics))
{
e.Handled = true;
}
}
catch (Exception ex)
{
// Consume the exception since it does not bubble up to our handlers
Trace.Write(ex);
DrawGraphCellFromCache(e.RowIndex, rowHeight, e.CellBounds, e.Graphics);
}
catch (Exception ex)
{
// Consume the exception since it does not bubble up to our handlers
Trace.Write(ex);
#if DEBUG
BugReportInvoker.LogError(ex);
BugReportInvoker.LogError(ex);
#endif
}
}
}

private bool PaintGraphCell(int rowIndex, int rowHeight, Rectangle cellBounds, Graphics graphics)
private bool DrawGraphCellFromCache(int rowIndex, int rowHeight, Rectangle cellBounds, Graphics graphics)
{
// Renders the required row into _graphCache.GraphBitmap if the row is available and not yet cached, and draws it from the cache.
// Draws the required row from the cache if available.

int height = _graphCache.Capacity * rowHeight;
int height = _graphDisplayCache.Capacity * rowHeight;
int width = Column.Width;

if (width <= 0 || height <= 0)
Expand All @@ -73,115 +64,141 @@ private bool PaintGraphCell(int rowIndex, int rowHeight, Rectangle cellBounds, G
return true;
}

if (_revisionGraph.GetSegmentsForRow(rowIndex) is null)
int offsetToHead = rowIndex - _graphDisplayCache.HeadRow;
if (offsetToHead < 0 || offsetToHead >= _graphDisplayCache.Count)
{
// Needs to be refreshed when available
// Item not in the cache
return false;
}

_graphCache.Allocate(Math.Max(width, GraphRenderer.LaneWidth * 3), height);
Rectangle cellRect = new(
0,
_graphDisplayCache.GetCacheRow(rowIndex) * rowHeight,
width,
rowHeight);

graphics.DrawImage(
_graphDisplayCache.GraphBitmap,
cellBounds,
cellRect,
GraphicsUnit.Pixel);

return true;
}

public async Task RenderGraphToCacheAsync(VisibleRowRange range, int toRowIndex, int rowHeight, CancellationToken cancellationToken)
{
DataGridView? control = Column.DataGridView;
if (control is null)
{
return;
}

RenderGraphToCache(range, toRowIndex, rowHeight);

await control.SwitchToMainThreadAsync(cancellationToken);

_graphDisplayCache.CopyFrom(_graphRenderCache);

cancellationToken.ThrowIfCancellationRequested();

if (Column.Width != _columnWidth)
{
Column.Width = _columnWidth;
}

control.InvalidateColumn(Column.Index);
}

private void RenderGraphToCache(VisibleRowRange range, int toRowIndex, int rowHeight)
{
int width = CalculateGraphColumnWidth(range);
if (_columnWidth != width)
{
_columnWidth = width;
_graphRenderCache.Reset();
}

int fromRowIndex = Math.Max(0, range.FromIndex - range.Count);
_graphRenderCache.AdjustCapacity(range.Count * 3);
int height = _graphRenderCache.Capacity * rowHeight;
_graphRenderCache.Allocate(Math.Max(_columnWidth, GraphRenderer.LaneWidth * 3), height);

for (int rowIndex = fromRowIndex; rowIndex <= toRowIndex; ++rowIndex)
{
RenderRowToCache(rowIndex, rowHeight);
}
}

private void RenderRowToCache(int rowIndex, int rowHeight)
{
// Renders the required row into _graphRenderCache.GraphBitmap if the row is available and not yet cached.

int startRow;
int endRow;
if (_graphCache.Count == 0)
if (_graphRenderCache.Count == 0)
{
// Start the cache with this line
startRow = rowIndex;
endRow = rowIndex + 1;
_graphCache.HeadRow = startRow;
_graphCache.Count = 1;
_graphRenderCache.HeadRow = startRow;
_graphRenderCache.Count = 1;
}
else
{
int offsetToHead = rowIndex - _graphCache.HeadRow;
if (offsetToHead >= 0 && offsetToHead < _graphCache.Count)
int offsetToHead = rowIndex - _graphRenderCache.HeadRow;
if (offsetToHead >= 0 && offsetToHead < _graphRenderCache.Count)
{
// Item already in the cache
DrawRectangleFromCache();
return true;
return;
}

if (offsetToHead < 0 && -offsetToHead < _graphCache.Capacity)
if (offsetToHead < 0 && -offsetToHead < _graphRenderCache.Capacity)
{
// Scroll back, make the current row the head row
startRow = rowIndex;
endRow = _graphCache.HeadRow;
_graphCache.HeadRow = startRow;
_graphCache.Count = Math.Min(_graphCache.Count + endRow - startRow, _graphCache.Capacity);
_graphCache.Head += _graphCache.Capacity + offsetToHead;
_graphCache.Head %= _graphCache.Capacity;
endRow = _graphRenderCache.HeadRow;
_graphRenderCache.HeadRow = startRow;
_graphRenderCache.Count = Math.Min(_graphRenderCache.Count + endRow - startRow, _graphRenderCache.Capacity);
_graphRenderCache.Head += _graphRenderCache.Capacity + offsetToHead;
_graphRenderCache.Head %= _graphRenderCache.Capacity;
}
else if (offsetToHead > 0 && offsetToHead <= 2 * (_graphCache.Capacity - 1))
else if (offsetToHead > 0 && offsetToHead <= 2 * (_graphRenderCache.Capacity - 1))
{
// Scroll forward
startRow = _graphCache.HeadRow + _graphCache.Count; // all rows before have already been rendered
startRow = _graphRenderCache.HeadRow + _graphRenderCache.Count; // all rows before have already been rendered
endRow = rowIndex + 1;
_graphCache.Count += endRow - startRow; // Count = Count + (rowIndex + 1) - (HeadRow + Count) = rowIndex + 1 - HeadRow
int neededHeadAdjustment = Math.Max(0, _graphCache.Count - _graphCache.Capacity);
_graphCache.Count -= neededHeadAdjustment;
_graphCache.HeadRow += neededHeadAdjustment;
_graphCache.Head += neededHeadAdjustment;
_graphCache.Head %= _graphCache.Capacity;
_graphRenderCache.Count += endRow - startRow; // Count = Count + (rowIndex + 1) - (HeadRow + Count) = rowIndex + 1 - HeadRow
int neededHeadAdjustment = Math.Max(0, _graphRenderCache.Count - _graphRenderCache.Capacity);
_graphRenderCache.Count -= neededHeadAdjustment;
_graphRenderCache.HeadRow += neededHeadAdjustment;
_graphRenderCache.Head += neededHeadAdjustment;
_graphRenderCache.Head %= _graphRenderCache.Capacity;
}
else
{
// Restart the cache with this line
startRow = rowIndex;
endRow = rowIndex + 1;
_graphCache.HeadRow = startRow;
_graphCache.Count = 1;
_graphRenderCache.HeadRow = startRow;
_graphRenderCache.Count = 1;
}
}

RenderVisibleGraphToCache();
DrawRectangleFromCache();
return true;

int GetCacheRow(int rowIndex) => (_graphCache.Head + rowIndex - _graphCache.HeadRow) % _graphCache.Capacity;

void DrawRectangleFromCache()
{
Rectangle cellRect = new(
0,
GetCacheRow(rowIndex) * rowHeight,
width,
rowHeight);

graphics.DrawImage(
_graphCache.GraphBitmap,
cellBounds,
cellRect,
GraphicsUnit.Pixel);
}

void RenderVisibleGraphToCache()
int x = ColumnLeftMargin;
int cellWidth = _columnWidth - ColumnLeftMargin;
Rectangle laneRect = new(x, 0, cellWidth, rowHeight);
for (rowIndex = startRow; rowIndex < endRow; ++rowIndex)
{
Validates.NotNull(_graphCache.GraphBitmapGraphics);
SmoothingMode oldSmoothingMode = _graphCache.GraphBitmapGraphics.SmoothingMode;
Region oldClip = _graphCache.GraphBitmapGraphics.Clip;
try
{
int x = ColumnLeftMargin;
int cellWidth = width - ColumnLeftMargin;
Rectangle laneRect = new(x, 0, cellWidth, rowHeight);
for (int rowIndex = startRow; rowIndex < endRow; ++rowIndex)
{
// Get the y coordinate of the current item's upper left in the cache
laneRect.Y = GetCacheRow(rowIndex) * rowHeight;
// Get the y coordinate of the current item's upper left in the cache
laneRect.Y = _graphRenderCache.GetCacheRow(rowIndex) * rowHeight;

using Region newClip = new(laneRect);
_graphCache.GraphBitmapGraphics.Clip = newClip;
using Region newClip = new(laneRect);
_graphRenderCache.GraphBitmapGraphics.Clip = newClip;

_graphCache.GraphBitmapGraphics.RenderingOrigin = new Point(x, laneRect.Y);
_graphRenderCache.GraphBitmapGraphics.RenderingOrigin = new Point(x, laneRect.Y);

GraphRenderer.DrawItem(_revisionGraph.Config, _graphCache.GraphBitmapGraphics, rowIndex, rowHeight, _revisionGraph.GetSegmentsForRow, RevisionGraphDrawStyle, _revisionGraph.HeadId);
}
}
finally
{
_graphCache.GraphBitmapGraphics.SmoothingMode = oldSmoothingMode;
_graphCache.GraphBitmapGraphics.Clip = oldClip;
}
GraphRenderer.DrawItem(_revisionGraph.Config, _graphRenderCache.GraphBitmapGraphics, rowIndex, rowHeight, _revisionGraph.GetSegmentsForRow, RevisionGraphDrawStyle, _revisionGraph.HeadId);
}
}

Expand All @@ -192,37 +209,15 @@ public override void ApplySettings()

public override void Clear()
{
_graphCache.Reset();
}

public override void OnColumnWidthChanged(DataGridViewColumnEventArgs e)
{
_graphCache.Reset();
_graphRenderCache.Reset();
_graphDisplayCache.Reset();
}

public void HighlightBranch(ObjectId id)
{
_revisionGraph.HighlightBranch(id);
}

public override void OnVisibleRowsChanged(in VisibleRowRange range)
{
if (!Column.Visible)
{
return;
}

// Keep an extra page in the cache
_graphCache.AdjustCapacity((range.Count * 2) + 1);

int width = CalculateGraphColumnWidth(range);
if (Column.Width != width)
{
Column.Width = width;
Column.DataGridView?.InvalidateColumn(Column.Index);
}
}

private int CalculateGraphColumnWidth(in VisibleRowRange range)
{
int maxLaneCount = range.Max(index => _revisionGraph.GetSegmentsForRow(index)?.GetLaneCount()) ?? 0;
Expand Down Expand Up @@ -261,10 +256,13 @@ internal TestAccessor(RevisionGraphColumnProvider revisionGraphColumnProvider)

internal RevisionGraphColumnProvider RevisionGraphColumnProvider { get; }

internal GraphCache GraphCache => RevisionGraphColumnProvider._graphCache;
internal GraphCache GraphCache => RevisionGraphColumnProvider._graphRenderCache;

internal void RenderGraphToCache(VisibleRowRange range, int toRowIndex, int rowHeight)
=> RevisionGraphColumnProvider.RenderGraphToCache(range, toRowIndex, rowHeight);

internal bool PaintGraphCell(int rowIndex, int rowHeight, Rectangle cellBounds, Graphics graphics)
=> RevisionGraphColumnProvider.PaintGraphCell(rowIndex, rowHeight, cellBounds, graphics);
internal void RenderRowToCache(int rowIndex, int rowHeight)
=> RevisionGraphColumnProvider.RenderRowToCache(rowIndex, rowHeight);
}
}
}
19 changes: 19 additions & 0 deletions GitUI/UserControls/RevisionGrid/Graph/Rendering/GraphCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,25 @@ internal void Allocate(int width, int height)
Reset();
}

internal void CopyFrom(GraphCache source)
{
Capacity = source.Capacity;
Bitmap sourceBitmap = source.GraphBitmap;
Allocate(sourceBitmap.Width, sourceBitmap.Height);
GraphBitmapGraphics.CompositingMode = CompositingMode.SourceCopy;
GraphBitmapGraphics.DrawImage(sourceBitmap, 0, 0);
Count = source.Count;
Head = source.Head;
HeadRow = source.HeadRow;
}

/// <summary>
/// Maps a graph row to a cache row.
/// </summary>
/// <param name="rowIndex">The row index in the entire graph.</param>
/// <returns>The row index in the cache.</returns>
internal int GetCacheRow(int rowIndex) => (Head + rowIndex - HeadRow) % Capacity;

internal void Reset()
{
Head = 0;
Expand Down