191 lines
6.0 KiB
C#
191 lines
6.0 KiB
C#
|
|
using System.IO;
|
||
|
|
using UnityEngine;
|
||
|
|
|
||
|
|
namespace Unity.PlasticSCM.Editor.UI
|
||
|
|
{
|
||
|
|
internal interface ICalcTextSize
|
||
|
|
{
|
||
|
|
float GetTextWidth(string text);
|
||
|
|
}
|
||
|
|
|
||
|
|
internal class CalcTextSize : ICalcTextSize
|
||
|
|
{
|
||
|
|
internal static ICalcTextSize FromStyle(GUIStyle style)
|
||
|
|
{
|
||
|
|
return new CalcTextSize(style);
|
||
|
|
}
|
||
|
|
|
||
|
|
CalcTextSize(GUIStyle style)
|
||
|
|
{
|
||
|
|
mStyle = style;
|
||
|
|
}
|
||
|
|
|
||
|
|
float ICalcTextSize.GetTextWidth(string text)
|
||
|
|
{
|
||
|
|
return mStyle.CalcSize(new GUIContent(text)).x;
|
||
|
|
}
|
||
|
|
|
||
|
|
readonly GUIStyle mStyle;
|
||
|
|
}
|
||
|
|
|
||
|
|
internal static class PathTrimming
|
||
|
|
{
|
||
|
|
/// <summary>
|
||
|
|
/// Truncates a path to fit within the available width using binary search.
|
||
|
|
/// Always preserves the beginning and end of the path when truncating.
|
||
|
|
/// </summary>
|
||
|
|
internal static string TruncatePath(
|
||
|
|
string path,
|
||
|
|
float availableWidth,
|
||
|
|
ICalcTextSize calcTextSize,
|
||
|
|
out bool wasTrimmed)
|
||
|
|
{
|
||
|
|
wasTrimmed = false;
|
||
|
|
|
||
|
|
if (availableWidth <= 0)
|
||
|
|
return string.Empty;
|
||
|
|
|
||
|
|
float fullWidth = calcTextSize.GetTextWidth(path);
|
||
|
|
|
||
|
|
// Path fits completely
|
||
|
|
if (fullWidth <= availableWidth)
|
||
|
|
return path;
|
||
|
|
|
||
|
|
wasTrimmed = true;
|
||
|
|
|
||
|
|
// Binary search for the optimal character count
|
||
|
|
int minChars = 1;
|
||
|
|
int maxChars = path.Length;
|
||
|
|
int bestLength = 0;
|
||
|
|
string bestFit = string.Empty;
|
||
|
|
|
||
|
|
while (minChars <= maxChars)
|
||
|
|
{
|
||
|
|
int midLength = (minChars + maxChars) / 2;
|
||
|
|
string truncated = TruncatePathByLength(path, midLength);
|
||
|
|
float width = calcTextSize.GetTextWidth(truncated);
|
||
|
|
|
||
|
|
if (width <= availableWidth)
|
||
|
|
{
|
||
|
|
// This fits, try to fit more
|
||
|
|
bestLength = midLength;
|
||
|
|
bestFit = truncated;
|
||
|
|
minChars = midLength + 1;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Too wide, try less
|
||
|
|
maxChars = midLength - 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
// If we couldn't fit anything reasonable, ensure at least some content is shown
|
||
|
|
if (bestLength == 0 || string.IsNullOrEmpty(bestFit))
|
||
|
|
{
|
||
|
|
// Show at least part of the filename
|
||
|
|
int lastSeparator = FindLastSeparator(path);
|
||
|
|
if (lastSeparator != -1)
|
||
|
|
{
|
||
|
|
string filename = path.Substring(lastSeparator + 1);
|
||
|
|
|
||
|
|
// Try to fit just the filename with middle truncation
|
||
|
|
minChars = 1;
|
||
|
|
maxChars = filename.Length;
|
||
|
|
|
||
|
|
while (minChars <= maxChars)
|
||
|
|
{
|
||
|
|
int midLength = (minChars + maxChars) / 2;
|
||
|
|
string truncated = TruncateMid(filename, midLength);
|
||
|
|
float width = calcTextSize.GetTextWidth(truncated);
|
||
|
|
|
||
|
|
if (width <= availableWidth)
|
||
|
|
{
|
||
|
|
bestFit = truncated;
|
||
|
|
minChars = midLength + 1;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
maxChars = midLength - 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// No path separator, just truncate the middle
|
||
|
|
bestFit = TruncateMid(path, 3);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return bestFit;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Finds the last occurrence of either forward slash or backslash separator.
|
||
|
|
/// Works cross-platform for both Windows and Unix-style paths.
|
||
|
|
/// </summary>
|
||
|
|
static int FindLastSeparator(string path)
|
||
|
|
{
|
||
|
|
int lastBackslash = path.LastIndexOf('\\');
|
||
|
|
int lastForwardSlash = path.LastIndexOf('/');
|
||
|
|
return Mathf.Max(lastBackslash, lastForwardSlash);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Truncates the given string to the number of characters given by the length parameter.
|
||
|
|
/// The value is truncated (if necessary) by removing characters from the middle of the
|
||
|
|
/// string and inserting an ellipsis in their place.
|
||
|
|
/// </summary>
|
||
|
|
static string TruncateMid(string value, int length)
|
||
|
|
{
|
||
|
|
if (value.Length <= length)
|
||
|
|
return value;
|
||
|
|
|
||
|
|
if (length <= 0)
|
||
|
|
return string.Empty;
|
||
|
|
|
||
|
|
if (length == 1)
|
||
|
|
return ELLIPSIS;
|
||
|
|
|
||
|
|
int mid = (length - 1) / 2;
|
||
|
|
string pre = value.Substring(0, Mathf.FloorToInt(mid));
|
||
|
|
string post = value.Substring(value.Length - Mathf.CeilToInt(mid));
|
||
|
|
|
||
|
|
return string.Concat(pre, ELLIPSIS, post);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// String truncation for paths.
|
||
|
|
/// This method takes a path and returns it truncated (if necessary) to the exact
|
||
|
|
/// number of characters specified by the length parameter.
|
||
|
|
/// </summary>
|
||
|
|
static string TruncatePathByLength(string path, int length)
|
||
|
|
{
|
||
|
|
if (path.Length <= length)
|
||
|
|
return path;
|
||
|
|
|
||
|
|
if (length <= 0)
|
||
|
|
return string.Empty;
|
||
|
|
|
||
|
|
if (length == 1)
|
||
|
|
return ELLIPSIS;
|
||
|
|
|
||
|
|
int lastSeparator = FindLastSeparator(path);
|
||
|
|
|
||
|
|
// No directory prefix, fall back to middle ellipsis
|
||
|
|
if (lastSeparator == -1)
|
||
|
|
return TruncateMid(path, length);
|
||
|
|
|
||
|
|
int filenameLength = path.Length - lastSeparator - 1;
|
||
|
|
|
||
|
|
// File name prefixed with …/ would be too long, fall back to middle ellipsis
|
||
|
|
if (filenameLength + 2 > length)
|
||
|
|
return TruncateMid(path, length);
|
||
|
|
|
||
|
|
string pre = path.Substring(0, length - filenameLength - 2);
|
||
|
|
string post = path.Substring(lastSeparator);
|
||
|
|
|
||
|
|
return string.Concat(pre, ELLIPSIS, post);
|
||
|
|
}
|
||
|
|
|
||
|
|
const string ELLIPSIS = "…";
|
||
|
|
}
|
||
|
|
}
|