#nullable disable
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
namespace WPFDark.StandardControls.Internal
{
// Based on this code
// https://github.com/dotnet/wpf/blob/master/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/Primitives/TabPanel.cs
public class TabPanelInternal : TabPanel
{
static TabPanelInternal()
{
KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(TabPanelInternal), new FrameworkPropertyMetadata(KeyboardNavigationMode.Once));
KeyboardNavigation.DirectionalNavigationProperty.OverrideMetadata(typeof(TabPanelInternal), new FrameworkPropertyMetadata(KeyboardNavigationMode.Cycle));
}
///
/// Updates DesiredSize of the TabPanelInternal. Called by parent UIElement. This is the first pass of layout.
///
///
/// TabPanelInternal
///
/// Constraint size is an "upper limit" that TabPanelInternal should not exceed.
/// TabPanelInternal' desired size.
protected override Size MeasureOverride(Size constraint)
{
var contentSize = new Size();
var tabAlignment = TabStripPlacement;
_numRows = 1;
_numHeaders = 0;
_rowHeight = 0;
switch (tabAlignment)
{
// For top and bottom placement the panel flow its children to calculate the number of rows and
// desired vertical size
case Dock.Top:
case Dock.Bottom:
{
var numInCurrentRow = 0;
var currentRowWidth = 0.0;
var maxRowWidth = 0.0;
foreach (UIElement child in InternalChildren)
{
if (child.Visibility == Visibility.Collapsed)
continue;
_numHeaders++;
// Helper measures child, and deals with Min, Max, and base Width & Height properties.
// Helper returns the size a child needs to take up (DesiredSize or property specified size).
child.Measure(constraint);
var childSize = GetDesiredSizeWithoutMargin(child);
if (_rowHeight < childSize.Height)
_rowHeight = childSize.Height;
if (currentRowWidth + childSize.Width > constraint.Width && numInCurrentRow > 0)
{
// If child does not fit in the current row - create a new row
if (maxRowWidth < currentRowWidth)
maxRowWidth = currentRowWidth;
currentRowWidth = childSize.Width;
numInCurrentRow = 1;
_numRows++;
}
else
{
currentRowWidth += childSize.Width;
numInCurrentRow++;
}
}
if (maxRowWidth < currentRowWidth)
maxRowWidth = currentRowWidth;
contentSize.Height = _rowHeight * _numRows;
// If we don't have constraint or content width is smaller than constraint width then size to content
if (double.IsInfinity(contentSize.Width) || double.IsNaN(contentSize.Width) ||
maxRowWidth < constraint.Width)
contentSize.Width = maxRowWidth;
else
contentSize.Width = constraint.Width;
break;
}
case Dock.Left:
case Dock.Right:
{
foreach (UIElement child in InternalChildren)
{
if (child.Visibility == Visibility.Collapsed)
continue;
_numHeaders++;
// Helper measures child, and deals with Min, Max, and base Width & Height properties.
// Helper returns the size a child needs to take up (DesiredSize or property specified size).
child.Measure(constraint);
var childSize = GetDesiredSizeWithoutMargin(child);
if (contentSize.Width < childSize.Width)
contentSize.Width = childSize.Width;
contentSize.Height += childSize.Height;
}
break;
}
default:
throw new ArgumentOutOfRangeException();
}
// Returns our minimum size & sets DesiredSize.
return contentSize;
}
///
/// TabPanelInternal arranges each of its children.
///
/// Size that TabPanelInternal will assume to position children.
protected override Size ArrangeOverride(Size arrangeSize)
{
var tabAlignment = TabStripPlacement;
switch (tabAlignment)
{
case Dock.Top:
case Dock.Bottom:
ArrangeHorizontal(arrangeSize);
break;
case Dock.Left:
case Dock.Right:
ArrangeVertical(arrangeSize);
break;
default:
throw new ArgumentOutOfRangeException();
}
return arrangeSize;
}
///
/// Override of .
///
/// Geometry to use as additional clip in case when element is larger then available space
protected override Geometry GetLayoutClip(Size layoutSlotSize)
{
return null;
}
private static Size GetDesiredSizeWithoutMargin(UIElement element)
{
var margin = (Thickness) element.GetValue(MarginProperty);
Size desiredSizeWithoutMargin = default;
desiredSizeWithoutMargin.Height = Math.Max(0d, element.DesiredSize.Height - margin.Top - margin.Bottom);
desiredSizeWithoutMargin.Width = Math.Max(0d, element.DesiredSize.Width - margin.Left - margin.Right);
return desiredSizeWithoutMargin;
}
private void GetHeadersSize(Span headerSize)
{
var childIndex = 0;
foreach (UIElement child in InternalChildren)
{
if (child.Visibility == Visibility.Collapsed)
continue;
var childSize = GetDesiredSizeWithoutMargin(child);
headerSize[childIndex] = Math.Floor(childSize.Width);
childIndex++;
}
}
private void ArrangeHorizontal(Size arrangeSize)
{
var numSeparators = _numRows - 1;
// ReSharper disable MergeConditionalExpression
var bufferSize =
Unsafe.SizeOf() * _numHeaders +
Unsafe.SizeOf() * numSeparators;
var bufferArray =
bufferSize >= 512
? ArrayPool.Shared.Rent(bufferSize)
: null;
var buffer =
bufferArray != null
? bufferArray.AsSpan(0, bufferSize)
: stackalloc byte[bufferSize];
// ReSharper restore MergeConditionalExpression
var headerSize = MemoryMarshal.Cast(buffer.Slice(0, Unsafe.SizeOf() * _numHeaders));
var solution = MemoryMarshal.Cast(buffer.Slice(Unsafe.SizeOf() * _numHeaders, Unsafe.SizeOf() * numSeparators));
try
{
var tabAlignment = TabStripPlacement;
var isMultiRow = _numRows > 1;
var activeRow = 0;
var childOffset = new Vector();
GetHeadersSize(headerSize);
// If we have multirows, then calculate the best header distribution
if (isMultiRow)
{
solution = CalculateHeaderDistribution(arrangeSize.Width, headerSize, solution);
activeRow = GetActiveRow(solution);
childOffset.Y = tabAlignment switch
{
// TabPanelInternal starts to layout children depend on activeRow which should be always on bottom (top)
// The first row should start from Y = (_numRows - 1 - activeRow) * _rowHeight
Dock.Top => ((_numRows - 1 - activeRow) * _rowHeight),
Dock.Bottom when activeRow != 0 => ((_numRows - activeRow) * _rowHeight),
_ => childOffset.Y
};
}
else
{
solution = Array.Empty();
}
var childIndex = 0;
var separatorIndex = 0;
foreach (UIElement child in InternalChildren)
{
if (child.Visibility == Visibility.Collapsed)
continue;
var margin = (Thickness) child.GetValue(MarginProperty);
var leftOffset = margin.Left;
var rightOffset = margin.Right;
var topOffset = margin.Top;
var bottomOffset = margin.Bottom;
var lastHeaderInRow = isMultiRow &&
(separatorIndex < solution.Length && solution[separatorIndex] == childIndex ||
childIndex == _numHeaders - 1);
//Length left, top, right, bottom;
var cellSize = new Size(headerSize[childIndex], _rowHeight);
// Align the last header in the row; If headers are not aligned directional nav would not work correctly
if (lastHeaderInRow)
cellSize.Width = arrangeSize.Width - childOffset.X;
child.Arrange(new Rect(childOffset.X, childOffset.Y, cellSize.Width, cellSize.Height));
var childSize = cellSize;
childSize.Height = Math.Max(0d, childSize.Height - topOffset - bottomOffset);
childSize.Width = Math.Max(0d, childSize.Width - leftOffset - rightOffset);
// Calculate the offset for the next child
childOffset.X += cellSize.Width;
if (lastHeaderInRow)
{
if ((separatorIndex == activeRow && tabAlignment == Dock.Top) ||
(separatorIndex == activeRow - 1 && tabAlignment == Dock.Bottom))
childOffset.Y = 0d;
else
childOffset.Y += _rowHeight;
childOffset.X = 0d;
separatorIndex++;
}
childIndex++;
}
}
finally
{
if (bufferArray != null)
ArrayPool.Shared.Return(bufferArray);
}
}
private void ArrangeVertical(Size arrangeSize)
{
var childOffsetY = 0d;
foreach (UIElement child in InternalChildren)
{
if (child.Visibility != Visibility.Collapsed)
{
var childSize = GetDesiredSizeWithoutMargin(child);
child.Arrange(new Rect(0, childOffsetY, arrangeSize.Width, childSize.Height));
// Calculate the offset for the next child
childOffsetY += childSize.Height;
}
}
}
// Returns the row which contain the child with IsSelected==true
private int GetActiveRow(Span solution)
{
var activeRow = 0;
var childIndex = 0;
if (solution.Length > 0)
{
foreach (UIElement child in InternalChildren)
{
if (child.Visibility == Visibility.Collapsed)
continue;
var isActiveTab = (bool) child.GetValue(Selector.IsSelectedProperty);
if (isActiveTab)
return activeRow;
if (activeRow < solution.Length && solution[activeRow] == childIndex)
activeRow++;
childIndex++;
}
}
// If the is no selected element and alignment is Top - then the active row is the last row
if (TabStripPlacement == Dock.Top)
activeRow = _numRows - 1;
return activeRow;
}
/* TabPanelInternal layout calculation:
After measure call we have:
rowWidthLimit - width of the TabPanelInternal
Header[0..n-1] - headers
headerWidth[0..n-1] - header width
Calculated values:
numSeparators - number of separators between numSeparators+1 rows
rowWidth[0..numSeparators] - row width
rowHeaderCount[0..numSeparators] - Row Count = number of headers on that row
rowAverageGap[0..numSeparators] - Average Gap for the row i = (rowWidth - rowWidth[i])/rowHeaderCount[i]
currentSolution[0..numSeparators-1] - separator currentSolution[i]=x means Header[x] and h[x+1] are separated with new line
bestSolution[0..numSeparators-1] - keep the last Best Solution
bestSolutionRowAverageGap - keep the last Best Solution Average Gap
Between all separators distribution the best solution have minimum Average Gap -
this is the amount of pixels added to the header (to justify) in the row
How does it work:
First we flow the headers to calculate the number of necessary rows (numSeparators+1).
That means we need to insert numSeparators separators between n headers (numSeparators CalculateHeaderDistribution(double rowWidthLimit, Span headerWidth, Span bestSolution)
{
var numSeparators = _numRows - 1;
// ReSharper disable MergeConditionalExpression
var doubleArraySize = _numRows + _numRows + _numRows;
var intArraySize = numSeparators + _numRows;
var doubleArray =
doubleArraySize > 128
? ArrayPool.Shared.Rent(_numRows + _numRows + _numRows)
: null;
var intArray =
intArraySize > 128
? ArrayPool.Shared.Rent(numSeparators + _numRows)
: null;
var doubleBuffer =
doubleArray != null
? doubleArray
: stackalloc double[doubleArraySize];
var intBuffer =
intArray != null
? intArray
: stackalloc int[intArraySize];
// ReSharper restore MergeConditionalExpression
var rowWidth = doubleBuffer.Slice(0, _numRows);
var rowAverageGap = doubleBuffer.Slice(_numRows, _numRows);
var bestSolutionRowAverageGap = doubleBuffer.Slice(_numRows + _numRows, _numRows);
var currentSolution = intBuffer.Slice(0, numSeparators);
var rowHeaderCount = intBuffer.Slice(numSeparators, _numRows);
try
{
var bestSolutionMaxRowAverageGap = 0.0;
var numHeaders = headerWidth.Length;
var currentRowWidth = 0.0;
var numberOfHeadersInCurrentRow = 0;
double currentAverageGap;
// Initialize the current state; Do the initial flow of the headers
var currentRowIndex = 0;
for (var index = 0; index < numHeaders; index++)
{
if (currentRowWidth + headerWidth[index] > rowWidthLimit && numberOfHeadersInCurrentRow > 0)
{
// if we cannot add next header - flow to next row
// Store current row before we go to the next
rowWidth[currentRowIndex] = currentRowWidth; // Store the current row width
rowHeaderCount[currentRowIndex] = numberOfHeadersInCurrentRow; // For each row we store the number os headers inside
currentAverageGap =
Math.Max(0d,
(rowWidthLimit - currentRowWidth) /
numberOfHeadersInCurrentRow); // The amount of width that should be added to justify the header
rowAverageGap[currentRowIndex] = currentAverageGap;
currentSolution[currentRowIndex] = index - 1; // Separator points to the last header in the row
if (bestSolutionMaxRowAverageGap < currentAverageGap
) // Remember the maximum of all currentAverageGap
bestSolutionMaxRowAverageGap = currentAverageGap;
// Iterate to next row
currentRowIndex++;
currentRowWidth = headerWidth[index]; // Accumulate header widths on the same row
numberOfHeadersInCurrentRow = 1;
}
else
{
currentRowWidth += headerWidth[index]; // Accumulate header widths on the same row
// Increase the number of headers only if they are not collapsed (width=0)
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (headerWidth[index] != 0)
numberOfHeadersInCurrentRow++;
}
}
// If everything fit in 1 row then exit (no separators needed)
if (currentRowIndex == 0)
return bestSolution.Slice(0, 0);
// Add the last row
rowWidth[currentRowIndex] = currentRowWidth;
rowHeaderCount[currentRowIndex] = numberOfHeadersInCurrentRow;
currentAverageGap = (rowWidthLimit - currentRowWidth) / numberOfHeadersInCurrentRow;
rowAverageGap[currentRowIndex] = currentAverageGap;
if (bestSolutionMaxRowAverageGap < currentAverageGap)
bestSolutionMaxRowAverageGap = currentAverageGap;
currentSolution.CopyTo(bestSolution); // Remember the first solution as initial bestSolution
rowAverageGap.CopyTo(bestSolutionRowAverageGap); // bestSolutionRowAverageGap is used in ArrangeOverride to calculate header sizes
// Search for the best solution
// The exit condition if when we cannot move header to the next row
while (true)
{
// Find the row with maximum AverageGap
var worstRowIndex = 0; // Keep the row index with maximum AverageGap
var maxAg = 0.0;
for (var i = 0; i < _numRows; i++) // for all rows
{
if (maxAg < rowAverageGap[i])
{
maxAg = rowAverageGap[i];
worstRowIndex = i;
}
}
// If we are on the first row - cannot move from previous
if (worstRowIndex == 0)
break;
// From the row with maximum AverageGap we try to move a header from previous row
var moveToRow = worstRowIndex;
var moveFromRow = moveToRow - 1;
var moveHeader = currentSolution[moveFromRow];
var movedHeaderWidth = headerWidth[moveHeader];
rowWidth[moveToRow] += movedHeaderWidth;
// If the moved header cannot fit - exit. We have the best solution already.
if (rowWidth[moveToRow] > rowWidthLimit)
break;
// If header is moved successfully to the worst row
// we update the arrays keeping the row state
currentSolution[moveFromRow]--;
rowHeaderCount[moveToRow]++;
rowWidth[moveFromRow] -= movedHeaderWidth;
rowHeaderCount[moveFromRow]--;
rowAverageGap[moveFromRow] = (rowWidthLimit - rowWidth[moveFromRow]) / rowHeaderCount[moveFromRow];
rowAverageGap[moveToRow] = (rowWidthLimit - rowWidth[moveToRow]) / rowHeaderCount[moveToRow];
// EvaluateSolution:
// If the current solution is better than bestSolution - keep it in bestSolution
maxAg = 0;
for (var i = 0; i < _numRows; i++) // for all rows
{
if (maxAg < rowAverageGap[i])
{
maxAg = rowAverageGap[i];
}
}
if (maxAg < bestSolutionMaxRowAverageGap)
{
bestSolutionMaxRowAverageGap = maxAg;
currentSolution.CopyTo(bestSolution);
rowAverageGap.CopyTo(bestSolutionRowAverageGap);
}
}
// Each header size should be increased so headers in the row stretch to fit the row
currentRowIndex = 0;
for (var index = 0; index < numHeaders; index++)
{
headerWidth[index] += bestSolutionRowAverageGap[currentRowIndex];
if (currentRowIndex < numSeparators && bestSolution[currentRowIndex] == index)
currentRowIndex++;
}
}
finally
{
if (doubleArray != null)
ArrayPool.Shared.Return(doubleArray);
if (intArray != null)
ArrayPool.Shared.Return(intArray);
}
return bestSolution;
}
private Dock TabStripPlacement => (TemplatedParent is TabControl tc)
? tc.TabStripPlacement
: Dock.Top;
private int _numRows = 1; // Number of row calculated in measure and used in arrange
private int _numHeaders; // Number of headers excluding the collapsed items
private double _rowHeight; // Maximum of all headers height
}
}