633 lines
25 KiB
C#
633 lines
25 KiB
C#
using UnityEngine;
|
|
using UnityEngine.Rendering;
|
|
using UnityEditor;
|
|
using UnityEngine.UIElements;
|
|
using UnityEditor.UIElements;
|
|
using System.Linq;
|
|
using Rive.EditorTools;
|
|
using System;
|
|
|
|
namespace Rive
|
|
{
|
|
[CustomEditor(typeof(Asset))]
|
|
public class AssetEditor : Editor
|
|
{
|
|
File m_file;
|
|
private Artboard m_artboard;
|
|
private StateMachine m_stateMachine;
|
|
private double m_lastTime = 0.0;
|
|
public override bool HasPreviewGUI() => true;
|
|
|
|
public override bool RequiresConstantRepaint()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
private enum AssetReferenceType
|
|
{
|
|
Embedded = 0,
|
|
Referenced = 1
|
|
}
|
|
|
|
|
|
|
|
public override VisualElement CreateInspectorGUI()
|
|
{
|
|
var root = new VisualElement();
|
|
var riveAsset = (Asset)target;
|
|
|
|
// File Assets Section
|
|
var embeddedFoldout = new Foldout { text = "File Assets", value = false };
|
|
root.Add(embeddedFoldout);
|
|
|
|
foreach (var embeddedAsset in riveAsset.EmbeddedAssets)
|
|
{
|
|
var assetContainer = new VisualElement();
|
|
assetContainer.style.paddingBottom = 30;
|
|
|
|
embeddedFoldout.Add(assetContainer);
|
|
|
|
// Asset Type
|
|
var enumField = new EnumField("Type:", embeddedAsset.AssetType);
|
|
enumField.SetEnabled(false);
|
|
assetContainer.Add(enumField);
|
|
|
|
// Asset Name
|
|
var nameField = new TextField("Name:") { value = embeddedAsset.Name };
|
|
// For text fields, make them readonly instead of using SetEnabled(false) to allow for copying the text
|
|
StyleAsReadonly(nameField);
|
|
nameField.isReadOnly = true;
|
|
assetContainer.Add(nameField);
|
|
|
|
// Asset ID
|
|
var idField = new TextField("ID:") { value = embeddedAsset.Id.ToString() };
|
|
StyleAsReadonly(idField);
|
|
idField.isReadOnly = true;
|
|
assetContainer.Add(idField);
|
|
|
|
// Asset Reference Type
|
|
var referenceType = embeddedAsset.InBandBytesSize > 0 ? AssetReferenceType.Embedded : AssetReferenceType.Referenced;
|
|
var referenceTypeField = new EnumField("Reference Type:", referenceType);
|
|
referenceTypeField.SetEnabled(false);
|
|
assetContainer.Add(referenceTypeField);
|
|
|
|
// Asset Data
|
|
if (referenceType == AssetReferenceType.Embedded)
|
|
{
|
|
var embeddedField = new TextField("Embedded Size:")
|
|
{
|
|
value = FormatBytes(embeddedAsset.InBandBytesSize),
|
|
tooltip = "The size of the asset data embedded in the Rive file."
|
|
};
|
|
StyleAsReadonly(embeddedField);
|
|
embeddedField.isReadOnly = true;
|
|
assetContainer.Add(embeddedField);
|
|
}
|
|
else
|
|
{
|
|
var assetField = new ObjectField("Referenced Asset")
|
|
{
|
|
objectType = GetAssetType(embeddedAsset.AssetType),
|
|
value = embeddedAsset.OutOfBandAsset,
|
|
};
|
|
|
|
// Allow referenced assets to be updated in the editor
|
|
assetField.RegisterValueChangedCallback(evt =>
|
|
{
|
|
|
|
var newValue = evt.newValue as OutOfBandAsset;
|
|
|
|
Asset asset = target as Asset;
|
|
|
|
if (asset == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Undo.RecordObject(this, "Updated Referenced Asset");
|
|
|
|
AssetImporter.SetOobAssetReference((Asset)target, embeddedAsset.Id, newValue);
|
|
|
|
});
|
|
assetContainer.Add(assetField);
|
|
}
|
|
}
|
|
|
|
|
|
// Artboard Metadata
|
|
if (riveAsset.EditorOnlyMetadata != null && riveAsset.EditorOnlyMetadata.Artboards.Count > 0)
|
|
{
|
|
var contentsFoldout = new Foldout { text = "Artboard Metadata", value = false };
|
|
root.Add(contentsFoldout);
|
|
|
|
for (int i = 0; i < riveAsset.EditorOnlyMetadata.Artboards.Count; i++)
|
|
{
|
|
bool isDefaultArtboard = i == 0;
|
|
var artboard = riveAsset.EditorOnlyMetadata.Artboards[i];
|
|
|
|
// Create a foldout for each artboard
|
|
string artboardLabel = artboard.Name + (isDefaultArtboard ? " (Default)" : "");
|
|
var artboardFoldout = new Foldout { text = artboardLabel, value = false };
|
|
artboardFoldout.style.paddingLeft = 8;
|
|
artboardFoldout.style.paddingRight = 8;
|
|
contentsFoldout.Add(artboardFoldout);
|
|
|
|
var artboardContainer = new VisualElement();
|
|
artboardFoldout.Add(artboardContainer);
|
|
|
|
|
|
AddCopyToClipboardMenu(artboardFoldout, artboard.Name, "Copy Artboard Name");
|
|
|
|
|
|
// Artboard Size
|
|
var sizeContainer = new VisualElement();
|
|
sizeContainer.style.flexDirection = FlexDirection.Row;
|
|
sizeContainer.style.marginLeft = 15;
|
|
sizeContainer.style.marginTop = 5;
|
|
artboardContainer.Add(sizeContainer);
|
|
|
|
var sizeLabel = new Label("Size:");
|
|
sizeLabel.style.marginRight = 8;
|
|
sizeContainer.Add(sizeLabel);
|
|
|
|
var sizeValueLabel = new Label($"{artboard.Width} x {artboard.Height}");
|
|
sizeContainer.Add(sizeValueLabel);
|
|
|
|
|
|
|
|
// State Machines Container
|
|
var stateMachinesContainer = new VisualElement();
|
|
stateMachinesContainer.style.marginLeft = 15;
|
|
stateMachinesContainer.style.marginTop = 10;
|
|
artboardContainer.Add(stateMachinesContainer);
|
|
|
|
foreach (var stateMachine in artboard.StateMachines)
|
|
{
|
|
var smContainer = new VisualElement();
|
|
smContainer.style.marginBottom = 10;
|
|
|
|
// State Machine Header
|
|
var smHeader = new VisualElement();
|
|
smHeader.style.flexDirection = FlexDirection.Row;
|
|
smHeader.style.alignItems = Align.Center;
|
|
smContainer.Add(smHeader);
|
|
|
|
var smLabel = new Label("State Machine:");
|
|
smLabel.style.marginRight = 8;
|
|
smHeader.Add(smLabel);
|
|
|
|
var smNameField = new TextField();
|
|
smNameField.value = stateMachine.Name;
|
|
StyleAsReadonly(smNameField);
|
|
smNameField.isReadOnly = true;
|
|
smNameField.SetEnabled(true);
|
|
smNameField.style.flexGrow = 1;
|
|
smHeader.Add(smNameField);
|
|
|
|
// Inputs
|
|
if (stateMachine.Inputs.Count > 0)
|
|
{
|
|
var inputsContainer = new VisualElement();
|
|
inputsContainer.style.marginLeft = 15;
|
|
inputsContainer.style.marginTop = 5;
|
|
smContainer.Add(inputsContainer);
|
|
|
|
var inputsLabel = new Label("Inputs:");
|
|
inputsLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
inputsLabel.style.marginBottom = 5;
|
|
inputsContainer.Add(inputsLabel);
|
|
|
|
foreach (var input in stateMachine.Inputs)
|
|
{
|
|
var inputContainer = new VisualElement();
|
|
inputContainer.style.flexDirection = FlexDirection.Row;
|
|
inputContainer.style.alignItems = Align.Center;
|
|
inputContainer.style.marginBottom = 2;
|
|
|
|
var typeLabel = new Label(input.Type);
|
|
typeLabel.style.marginRight = 8;
|
|
typeLabel.style.width = 60;
|
|
|
|
var nameField = new TextField();
|
|
nameField.value = input.Name;
|
|
StyleAsReadonly(nameField);
|
|
nameField.isReadOnly = true;
|
|
nameField.SetEnabled(true);
|
|
nameField.style.flexGrow = 1;
|
|
|
|
inputContainer.Add(typeLabel);
|
|
inputContainer.Add(nameField);
|
|
inputsContainer.Add(inputContainer);
|
|
}
|
|
}
|
|
|
|
stateMachinesContainer.Add(smContainer);
|
|
}
|
|
|
|
if (artboard.DefaultViewModel != null && !String.IsNullOrEmpty(artboard.DefaultViewModel.Name))
|
|
{
|
|
var defaultVMContainer = new VisualElement();
|
|
defaultVMContainer.style.flexDirection = FlexDirection.Row;
|
|
defaultVMContainer.style.alignItems = Align.Center;
|
|
|
|
defaultVMContainer.style.marginLeft = 15;
|
|
defaultVMContainer.style.marginBottom = 5;
|
|
artboardContainer.Add(defaultVMContainer);
|
|
|
|
var defaultVMLabel = new Label("Default View Model:");
|
|
defaultVMLabel.style.marginRight = 8;
|
|
defaultVMContainer.Add(defaultVMLabel);
|
|
|
|
var defaultVMNameField = new TextField();
|
|
defaultVMNameField.value = artboard.DefaultViewModel.Name;
|
|
StyleAsReadonly(defaultVMNameField);
|
|
defaultVMNameField.isReadOnly = true;
|
|
defaultVMNameField.SetEnabled(true);
|
|
defaultVMNameField.style.flexGrow = 1;
|
|
defaultVMContainer.Add(defaultVMNameField);
|
|
}
|
|
}
|
|
}
|
|
|
|
// View Models Section
|
|
|
|
if (riveAsset.EditorOnlyMetadata != null && riveAsset.EditorOnlyMetadata.ViewModels.Count > 0)
|
|
{
|
|
var viewModelsFoldout = new Foldout { text = "View Models", value = false };
|
|
|
|
root.Add(viewModelsFoldout);
|
|
|
|
foreach (var viewModel in riveAsset.EditorOnlyMetadata.ViewModels)
|
|
{
|
|
var viewModelFoldout = new Foldout { text = viewModel.Name, value = false };
|
|
viewModelFoldout.style.paddingLeft = 8;
|
|
viewModelFoldout.style.paddingRight = 8;
|
|
viewModelsFoldout.Add(viewModelFoldout);
|
|
AddCopyToClipboardMenu(viewModelFoldout, viewModel.Name, "Copy View Model Name");
|
|
|
|
// Properties
|
|
if (viewModel.Properties.Count > 0)
|
|
{
|
|
var propertiesContainer = new VisualElement();
|
|
propertiesContainer.style.marginLeft = 15;
|
|
propertiesContainer.style.marginTop = 5;
|
|
viewModelFoldout.Add(propertiesContainer);
|
|
|
|
var propertiesLabel = new Label("Properties:");
|
|
propertiesLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
propertiesLabel.style.marginBottom = 5;
|
|
propertiesContainer.Add(propertiesLabel);
|
|
|
|
foreach (var property in viewModel.Properties)
|
|
{
|
|
var propertyContainer = new VisualElement();
|
|
propertyContainer.style.flexDirection = FlexDirection.Row;
|
|
propertyContainer.style.alignItems = Align.Center;
|
|
propertyContainer.style.marginBottom = 2;
|
|
|
|
var typeLabel = new Label(GetViewModelPropertyTypeLabel(property));
|
|
typeLabel.style.marginRight = 8;
|
|
typeLabel.style.minWidth = 60;
|
|
|
|
var nameField = new TextField();
|
|
nameField.value = property.Name;
|
|
StyleAsReadonly(nameField);
|
|
nameField.isReadOnly = true;
|
|
nameField.SetEnabled(true);
|
|
nameField.style.flexGrow = 1;
|
|
|
|
propertyContainer.Add(typeLabel);
|
|
propertyContainer.Add(nameField);
|
|
propertiesContainer.Add(propertyContainer);
|
|
}
|
|
}
|
|
|
|
// Instance Names
|
|
if (viewModel.InstanceNames.Count > 0)
|
|
{
|
|
var instancesContainer = new VisualElement();
|
|
instancesContainer.style.marginLeft = 15;
|
|
instancesContainer.style.marginTop = 10;
|
|
instancesContainer.style.marginBottom = 10;
|
|
viewModelFoldout.Add(instancesContainer);
|
|
|
|
var instancesLabel = new Label("Instances:");
|
|
instancesLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
instancesLabel.style.marginBottom = 5;
|
|
instancesContainer.Add(instancesLabel);
|
|
|
|
foreach (var instanceName in viewModel.InstanceNames)
|
|
{
|
|
var instanceField = new TextField();
|
|
instanceField.value = instanceName;
|
|
StyleAsReadonly(instanceField);
|
|
instanceField.isReadOnly = true;
|
|
instanceField.SetEnabled(true);
|
|
instancesContainer.Add(instanceField);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Enums Section
|
|
|
|
if (riveAsset.EditorOnlyMetadata.Enums.Count > 0)
|
|
{
|
|
var enumsFoldout = new Foldout { text = "Enums", value = false };
|
|
root.Add(enumsFoldout);
|
|
|
|
foreach (var enumData in riveAsset.EditorOnlyMetadata.Enums)
|
|
{
|
|
// Create a foldout for each enum type
|
|
var enumFoldout = new Foldout { text = enumData.Name, value = false };
|
|
enumFoldout.style.paddingLeft = 8;
|
|
enumFoldout.style.paddingRight = 8;
|
|
enumsFoldout.Add(enumFoldout);
|
|
AddCopyToClipboardMenu(enumFoldout, enumData.Name, "Copy Enum Name");
|
|
|
|
// Values
|
|
var valuesContainer = new VisualElement();
|
|
valuesContainer.style.marginLeft = 15;
|
|
valuesContainer.style.marginTop = 5;
|
|
valuesContainer.style.marginBottom = 10;
|
|
enumFoldout.Add(valuesContainer);
|
|
|
|
var valuesLabel = new Label("Values:");
|
|
valuesLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
valuesLabel.style.marginBottom = 5;
|
|
valuesContainer.Add(valuesLabel);
|
|
|
|
foreach (var value in enumData.Values)
|
|
{
|
|
var valueField = new TextField();
|
|
valueField.value = value;
|
|
StyleAsReadonly(valueField);
|
|
valueField.isReadOnly = true;
|
|
valueField.SetEnabled(true);
|
|
valuesContainer.Add(valueField);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return root;
|
|
}
|
|
|
|
|
|
private void AddCopyToClipboardMenu(Foldout foldout, string textToCopy, string itemLabel = null)
|
|
{
|
|
if (string.IsNullOrEmpty(textToCopy))
|
|
{
|
|
return;
|
|
}
|
|
|
|
itemLabel = itemLabel ?? $"Copy \"{foldout.text}\"";
|
|
|
|
foldout.AddManipulator(new ContextualMenuManipulator((ContextualMenuPopulateEvent evt) =>
|
|
{
|
|
evt.menu.AppendAction(itemLabel, (action) =>
|
|
{
|
|
GUIUtility.systemCopyBuffer = textToCopy;
|
|
});
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
private string GetViewModelPropertyTypeLabel(FileMetadata.ViewModelPropertyMetadata property)
|
|
{
|
|
// We want to display the type of the property, and if it's a ViewModel type, we also want to display the nested ViewModel name.
|
|
if (property.Type == ViewModelDataType.ViewModel)
|
|
{
|
|
return $"{property.Type.ToString()} ({property.NestedViewModelName})";
|
|
}
|
|
else if (property.Type == ViewModelDataType.Enum && !string.IsNullOrEmpty(property.EnumTypeName))
|
|
{
|
|
return $"{property.Type.ToString()} ({property.EnumTypeName})";
|
|
}
|
|
|
|
return $"{property.Type.ToString()}";
|
|
}
|
|
|
|
private void StyleAsReadonly(VisualElement element)
|
|
{
|
|
element.style.opacity = 0.5f;
|
|
}
|
|
|
|
private System.Type GetAssetType(EmbeddedAssetType assetType)
|
|
{
|
|
switch (assetType)
|
|
{
|
|
case EmbeddedAssetType.Font:
|
|
return typeof(FontOutOfBandAsset);
|
|
case EmbeddedAssetType.Image:
|
|
return typeof(ImageOutOfBandAsset);
|
|
case EmbeddedAssetType.Audio:
|
|
return typeof(AudioOutOfBandAsset);
|
|
default:
|
|
return typeof(System.Object);
|
|
}
|
|
}
|
|
|
|
|
|
public override Texture2D RenderStaticPreview(
|
|
string assetPath,
|
|
UnityEngine.Object[] subAssets,
|
|
int width,
|
|
int height
|
|
)
|
|
{
|
|
RenderTexture prev = RenderTexture.active;
|
|
var rect = new Rect(0, 0, width, height);
|
|
RenderTexture rt = Render(rect, true);
|
|
|
|
if (rt != null)
|
|
{
|
|
RenderTexture sourceForRead = rt;
|
|
RenderTexture temp = null;
|
|
|
|
// Static preview: use the runtime decode material (Rive/UI/Default) in Linear color space.
|
|
// This decodes gamma to linear, which works correctly with ReadPixels→Texture2D path.
|
|
// We DON'T use the pass-through shader here because the blit+ReadPixels seems to cause issues, and leads to nothing rendering for the static preview.
|
|
// TODO: Remove this once we have a proper way to display the texture in Linear color space.
|
|
if (Rive.TextureHelper.ProjectNeedsColorSpaceFix)
|
|
{
|
|
var mat = Rive.TextureHelper.GammaToLinearUIMaterial;
|
|
if (mat != null)
|
|
{
|
|
temp = RenderTexture.GetTemporary(rt.width, rt.height, 0, RenderTextureFormat.ARGB32);
|
|
Graphics.Blit(rt, temp, mat);
|
|
sourceForRead = temp;
|
|
}
|
|
}
|
|
|
|
RenderTexture.active = sourceForRead;
|
|
|
|
Texture2D tex = new Texture2D(width, height);
|
|
tex.ReadPixels(rect, 0, 0);
|
|
tex.Apply(true);
|
|
|
|
RenderTexture.active = prev;
|
|
if (temp != null)
|
|
{
|
|
RenderTexture.ReleaseTemporary(temp);
|
|
}
|
|
RenderTexture.ReleaseTemporary(rt);
|
|
return tex;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
RenderTexture Render(Rect rect, bool isStatic = false)
|
|
{
|
|
int width = (int)rect.width;
|
|
int height = (int)rect.height;
|
|
|
|
var descriptor = Rive.TextureHelper.Descriptor(width, height);
|
|
RenderTexture rt = RenderTexture.GetTemporary(descriptor);
|
|
|
|
var cmb = new CommandBuffer();
|
|
|
|
cmb.SetRenderTarget(rt);
|
|
|
|
if (m_file == null)
|
|
{
|
|
var riveAsset = (Rive.Asset)target;
|
|
m_file = Rive.File.Load(riveAsset);
|
|
m_artboard = m_file?.Artboard(0);
|
|
m_stateMachine = m_artboard?.StateMachine();
|
|
}
|
|
|
|
if (m_artboard != null)
|
|
{
|
|
var rq = new RenderQueue(
|
|
UnityEngine.SystemInfo.graphicsDeviceType == GraphicsDeviceType.Metal
|
|
? null
|
|
: rt
|
|
);
|
|
var renderer = rq.Renderer();
|
|
renderer.Align(Fit.Contain, Alignment.Center, m_artboard);
|
|
renderer.Draw(m_artboard);
|
|
renderer.AddToCommandBuffer(cmb);
|
|
if (!isStatic)
|
|
{
|
|
var now = EditorApplication.timeSinceStartup;
|
|
double time = now - m_lastTime;
|
|
m_stateMachine?.Advance((float)(now - m_lastTime));
|
|
m_lastTime = now;
|
|
}
|
|
else
|
|
{
|
|
m_stateMachine?.Advance(0.0f);
|
|
}
|
|
}
|
|
var prev = RenderTexture.active;
|
|
Graphics.ExecuteCommandBuffer(cmb);
|
|
GL.InvalidateState();
|
|
cmb.Clear();
|
|
|
|
if (isStatic && FlipY())
|
|
{
|
|
RenderTexture temp = RenderTexture.GetTemporary(
|
|
width,
|
|
height,
|
|
0,
|
|
RenderTextureFormat.Default,
|
|
RenderTextureReadWrite.Default
|
|
);
|
|
temp.Create();
|
|
|
|
Graphics.Blit(rt, temp, new Vector2(1, -1), new Vector2(0, 1));
|
|
RenderTexture.ReleaseTemporary(rt);
|
|
rt = temp;
|
|
}
|
|
|
|
// Caller releases the temporary RT
|
|
return rt;
|
|
}
|
|
|
|
public override void OnPreviewGUI(Rect rect, GUIStyle background)
|
|
{
|
|
if (Event.current.type == EventType.Repaint)
|
|
{
|
|
RenderTexture rt = Render(rect);
|
|
|
|
var drawRect = FlipY()
|
|
? new Rect(rect.x, rect.y + rect.height, rect.width, -rect.height)
|
|
: rect;
|
|
|
|
// Live preview: use a simple pass-through shader in Linear color space.
|
|
// Rive outputs gamma values, and it looks like EditorGUI.DrawPreviewTexture expects sRGB input.
|
|
// We DON'T use the decode material here because it would decode to linear, causing burnt/dark colors.
|
|
// The pass-through shader (Hidden/Rive/Editor/SRGBEncodePreview) just returns the texture unchanged.
|
|
// TODO: Remove this once we have a proper way to display the texture in Linear color space.
|
|
var mat = (Rive.TextureHelper.ProjectNeedsColorSpaceFix ? GetEncodePreviewMaterial() : null);
|
|
UnityEditor.EditorGUI.DrawPreviewTexture(drawRect, rt, mat);
|
|
RenderTexture.ReleaseTemporary(rt);
|
|
}
|
|
}
|
|
|
|
private static Material s_encodePreviewMaterial;
|
|
private static Material GetEncodePreviewMaterial()
|
|
{
|
|
if (s_encodePreviewMaterial != null) return s_encodePreviewMaterial;
|
|
var shader = Shader.Find("Hidden/Rive/Editor/SRGBEncodePreview");
|
|
if (shader == null) return null;
|
|
s_encodePreviewMaterial = new Material(shader)
|
|
{
|
|
name = "Rive_Editor_SRGBEncodePreview",
|
|
hideFlags = HideFlags.HideAndDontSave
|
|
};
|
|
return s_encodePreviewMaterial;
|
|
}
|
|
|
|
private void UnloadPreview()
|
|
{
|
|
m_stateMachine = null;
|
|
m_artboard = null;
|
|
if (m_file != null)
|
|
{
|
|
m_file.Dispose();
|
|
m_file = null;
|
|
}
|
|
}
|
|
|
|
public void OnDisable()
|
|
{
|
|
var riveAsset = (Rive.Asset)target;
|
|
UnloadPreview();
|
|
}
|
|
|
|
private static bool FlipY()
|
|
{
|
|
switch (UnityEngine.SystemInfo.graphicsDeviceType)
|
|
{
|
|
case GraphicsDeviceType.Metal:
|
|
case GraphicsDeviceType.Vulkan:
|
|
case GraphicsDeviceType.Direct3D11:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static string FormatBytes(uint byteCount)
|
|
{
|
|
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
|
|
int order = 0;
|
|
while (byteCount >= 1024 && order < sizes.Length - 1)
|
|
{
|
|
order++;
|
|
byteCount /= 1024;
|
|
}
|
|
|
|
// Adjust the format string to your preferences. For example "{0:0.#}{1}" would
|
|
// show a single decimal place, and no space.
|
|
return string.Format("{0:0.##} {1}", byteCount, sizes[order]);
|
|
}
|
|
}
|
|
}
|