Files
ShrlAlgoToolkit/WPFDark/StandardControls/Internal/TabPanelInternal.cs
ShrlAlgo 4d35cadb56 更新
2025-07-11 09:20:23 +08:00

568 lines
24 KiB
C#

#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));
}
/// <summary>
/// Updates DesiredSize of the TabPanelInternal. Called by parent UIElement. This is the first pass of layout.
/// </summary>
/// <remarks>
/// TabPanelInternal
/// </remarks>
/// <param name="constraint">Constraint size is an "upper limit" that TabPanelInternal should not exceed.</param>
/// <returns>TabPanelInternal' desired size.</returns>
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;
}
/// <summary>
/// TabPanelInternal arranges each of its children.
/// </summary>
/// <param name="arrangeSize">Size that TabPanelInternal will assume to position children.</param>
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;
}
/// <summary>
/// Override of <seealso cref="UIElement.GetLayoutClip"/>.
/// </summary>
/// <returns>Geometry to use as additional clip in case when element is larger then available space</returns>
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<double> 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<double>() * _numHeaders +
Unsafe.SizeOf<int>() * numSeparators;
var bufferArray =
bufferSize >= 512
? ArrayPool<byte>.Shared.Rent(bufferSize)
: null;
var buffer =
bufferArray != null
? bufferArray.AsSpan(0, bufferSize)
: stackalloc byte[bufferSize];
// ReSharper restore MergeConditionalExpression
var headerSize = MemoryMarshal.Cast<byte, double>(buffer.Slice(0, Unsafe.SizeOf<double>() * _numHeaders));
var solution = MemoryMarshal.Cast<byte, int>(buffer.Slice(Unsafe.SizeOf<double>() * _numHeaders, Unsafe.SizeOf<int>() * 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<int>();
}
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<byte>.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<int> 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<n always).
For each current state rowAverageGap[1..numSeparators+1] are calculated for each row.
Current state rowAverageGap = MAX (rowAverageGap[1..numSeparators+1]).
Our goal is to find the solution with MIN (rowAverageGap).
On each iteration step we move a header from a previous row to the row with maximum rowAverageGap.
We continue the iterations only if we move to better solution, i.e. rowAverageGap is smaller.
Maximum iteration steps are less the number of headers.
*/
// Input: Row width and width of all headers
// Output: int array which size is the number of separators and contains each separator position
private Span<int> CalculateHeaderDistribution(double rowWidthLimit, Span<double> headerWidth, Span<int> bestSolution)
{
var numSeparators = _numRows - 1;
// ReSharper disable MergeConditionalExpression
var doubleArraySize = _numRows + _numRows + _numRows;
var intArraySize = numSeparators + _numRows;
var doubleArray =
doubleArraySize > 128
? ArrayPool<double>.Shared.Rent(_numRows + _numRows + _numRows)
: null;
var intArray =
intArraySize > 128
? ArrayPool<int>.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<double>.Shared.Return(doubleArray);
if (intArray != null)
ArrayPool<int>.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
}
}