Search Unity

Better way in getting Clipping Text with dots

Discussion in 'Scripting' started by Rukas90, May 15, 2019.

  1. Rukas90

    Rukas90

    Joined:
    Sep 20, 2015
    Posts:
    169
    Hi there,
    I made this simple method to get clipping text with dots, it works great, but I feel like there has to be a better way in achieving this. I'm afraid it might become heavy on performance, if many elements (labels) are drawn that runs this function.

    P.S. This is for EditorWindow script.

    Does anyone have any tips on how could I improve this? Thank you
    Code (CSharp):
    1.  
    2.     private void OnGUI()
    3.     {
    4.         //Example usage..
    5.         Rect labelRect = new Rect(rect.x + 5, rect.y + rect.height, rect.width - 10, 20);
    6.         GUIStyle labelStyle = new GUIStyle(EditorStyles.label)
    7.         {
    8.             alignment = TextAnchor.MiddleLeft,
    9.             clipping = TextClipping.Clip,
    10.             wordWrap = false
    11.         };
    12.         GUI.Label(labelRect, Shortcut_Util.GetClippingText(obj.name, labelRect), labelStyle);
    13.     }
    14.  
    15.     const string ellipsis = "...";
    16.     public static string GetClippingText(string text, Rect area)
    17.     {
    18.         if (GUI.skin.label.CalcSize(new GUIContent(text)).x > area.width)
    19.         {
    20.             var clippedText = ellipsis;
    21.             var characters = text.ToCharArray();
    22.             for (int i = 0; i < characters.Length; i++)
    23.             {
    24.                 clippedText = clippedText.Insert(i, characters[i].ToString());
    25.                 if (GUI.skin.label.CalcSize(new GUIContent(clippedText)).x >= area.width)
    26.                 {
    27.                     break;
    28.                 }
    29.             }
    30.             return clippedText;
    31.         }
    32.         return text;
    33.     }
     
    Last edited: May 15, 2019
  2. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    The better way is to not use IMGUI (which is the GUI system that uses OnGUI etc). I'm going to guess you have followed a tutorial or something from circa 2012 that hasn't been updated since then. These days IMGUI is only recommended for editor scripting (even in that scenario its days are numbered). For runtime UI, it has godawful performance issues and looks and feels so much worse than Unity UI. If it's possible to switch to the better UI system (which is to say, if you're not at the finish line for your project), then you should start over on your UI using the Unity UI system (and there's plenty of tutorials on the Unity site for it). I know the prospect of scrapping what you have made and starting over is not appealing, but trust me when I say the headaches you'll save down the line will be worth taking the hit now.

    And more specifically relating to this question, for text in your UI, make a point to use TextMeshPro (which is fully supported in Unity UI and free). TMP has this functionality built in, no coding needed. See this page (search the page for "overflow").

    If you are going to stick with IMGUI knowing that there's a better alternative, then I'm guessing you're probably not the type to post a "this works but maybe there's a better way" question here, since IMGUI is the definition of "this works but there's a better way". But if you're in too deep to change it at this point in your project, then don't worry too much about finding the perfect truncation solution.
     
  3. Rukas90

    Rukas90

    Joined:
    Sep 20, 2015
    Posts:
    169
    Hey, thanks for the reply! God no, I wouldn't use OnGUI to display ingame UI :D Not that much of a rookie lol. Like you mentioned Unity's UI System is much better in that case of course. And no, this is not from the lesson and I should've explained more I guess, but I'm working on the EditorWindow script. So this is all within the editor stuff.
     
    Last edited: May 15, 2019
  4. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    Ah, gotcha. Be aware of the trap of premature optimization. Before you tweak for performance, stress-test the system in question to see if there is in fact a performance issue. Honestly if you don't anticipate more than one or two of these text boxes onscreen at any given time, just leave it as is.

    There's one non-performance-based change I would make, though. Right now, you're trying a bunch of strings until you find the first one that's too big, and you return that one. Rather, you should try a bunch of strings until you find the first one that's too big, then return the previous one, that wasn't too big. As is, your algorithm is going to produce inconsistent results; if your text is "mmmmmmm" it's likely to return a result that does actually visually exceed the width, while "llllllll" won't. It all depends on what the last character is.

    Your basic algorithm is on the right track, to try bigger things until they're too big.

    I think the main focus should be to minimize the number of times you call CalcSize, since that is going to be the major performance hog of your algorithm. (Additionally, building the string to send to CalcSize will create garbage as most string and array modifications do, which is a performance issue but one that matters less in Editor than in runtime.)

    There are a few ways to do that, but most of them will involve being a little bit more random-access with the construction of the "partial text plus ellipsis" string, so let's start by switching that to use Substring.
    Code (csharp):
    1. clippedText = text.Substring(0, n) + ellipsis;
    (where n is the number of characters you want)
    From there, I have two possible routes to suggest:

    First option: estimate, then move towards the right answer
    Starting with your initial CalcSize result on line 18 (which you should store), divide that into area.width to estimate, "the ellipsis is probably near 0.62 times the length of the string", so start with your n there. Then move n up or down by 1 until you get as close as possible.

    Second option: move in larger increments at first
    This is probably a little simpler to implement, but basically add an outer loop that increments n by 10, then when CalcSize exceeds the area width, back up by 10 and then increment by 1 instead.