How to apply multiple styles to the same line(s) of GUI text

I'd like to have a set of lines of GUI text with multiple styles in it. So long as I use only a single line of text, I can use Horizontal areas and GUILayout.Label to do coloring, but if any piece of the line flows over and "wrap"s (which is set in the style for that piece), the formatting is thrown off.

This happens because GUIlayout.Label's can't be started right after the last character in a textfield, only at the end of the text field, either to the right of it, or below it.

E.g.:

"I am the smartest person that"

[new style] "ever" [/new style]

"walked the earth, so I think."

What I want is to have something like:

"I am the smartest person that" [new style] "ever" [/new style] "walked the earth, so I think."

I could try count the number of characters and offset a manual GUI.Label (Rect(pixelCount, etc...), but that is far too inaccurate. Does anyone have a better way of doing this?

To rephrase my problem: I need a way to apply styles to portions of a string that wraps, similar to the way HTML tags work.

This guy, (http://forum.unity3d.com/viewtopic.php?t=9808), used the approximation technique, with 5 pixels per character and GUILayout.Label's instead of manual GUI.Label's.

1 Answer

1

After a little bit of work, I have a system that splits a string into a 3D array: the first dimension contains the entire string, split up into one line per element, the second dimension contains each part of line, as split up by arbitrary tags, and the third dimension contains the actual text and a cached ref to the style associated with it.

Anyway, to the example:

var notice = "Hello there. <blu>How are you? <nor>I'm fine, thank you for asking.  <blu>How would you like to be my pet? <red>Oh, I'd love that. <nor>I do believe I'm going crazy as all <blu>balls <nor>else.";

var messageToReturn = ParseStyles(notice, null); // We can pass a default style here

function ParseStyles(notice : String, style : GUIStyle)
{
  // <nor> = noticeNormal : GUIStyle
  // <blu> = noticeBlue : GUIStyle
  // <red> = noticeRed : GUIStyle

  if (style != null)
    var curStyle = style;
  else
    // Start with normal style
    curStyle = noticeNormal;

  var msg : Array = Array();
  var leftovers : String = notice;
  var cont : boolean = true;

  while (cont)
  {
    var container : Array = Array();
    var go : boolean = true;

    // My lines will only be ~39 chars long
    if (leftovers.length >= 39)
      var charsToGo : int = 39;
    else
    {
      charsToGo = leftovers.length-1;
      // We've reached the end, so do not continue
      cont = false;
    }

    var text : String = leftovers;

    while (go)
    {
      // --- Find indexes of tags within the first chunk (39 chars) of the string
      var normalIndex : int = text.IndexOf("<nor>",0, charsToGo);
      if (normalIndex == -1) normalIndex = 100; // Infinity doesn't work, but 100 is guaranteed to be higher than 39, our max index
      var blueIndex : int = text.IndexOf("<blu>",0, charsToGo);
      if (blueIndex == -1) blueIndex = 100;
      var redIndex : int = text.IndexOf("<red>",0, charsToGo);
      if (redIndex == -1) redIndex = 100;

      var lowest : int = Mathf.Min(normalIndex,blueIndex);
      lowest = Mathf.Min(lowest, redIndex);
      // ...etc. with all other styles compared to the previous lowest. May the lowest low win

      if (lowest > 0 && lowest < 39)
      {
        // Put what we have so far plus the style into the end of the array
        container.Add(Array(text.Substring(0,lowest), curStyle ));

        // Remove what we added to container
        text = text.Remove(0, lowest);

        // Calculate the new style for the next iteration of the while loop. Mine starts at 1 and goes 3 because I'm evaluating what's between the <___>
        var sub : String = text.Substring(1, 3);
        switch (sub)
        {
          case "nor":
            curStyle = noticeNormal;
          break;
          case "blu":
            curStyle = noticeBlue;
          break;
          case "red":
            curStyle = noticeRed;
          break;
        }

        // Remove the tag characters and subtract from charsToGo.  My tags are 5 chars long
        print ("before: "+text);
        text = text.Remove(0, 5);
        charsToGo -= lowest + 5;
      }

      else
      {
        // No styles found in this segment, so put the rest of the line in and get ready for tail recursion
        if (cont)
          // We have more to do, cut at the last space.
          var lastSpace = text.LastIndexOf(" ",charsToGo); // cut it off at the last space  
        else
          // We have naught else to do, manually set lastSpace to force the whole thing into container
          lastSpace = -1;

        // Now check lastSpace
        if (lastSpace != -1)
        {
          container.Add(Array(text.Substring(0,lastSpace), curStyle ));
          leftovers = text.Remove(0, lastSpace); // Cuts everything up to lastSpace
        }
        // There is nothing more to parse
        else
        {
          container.Add(Array(text, curStyle ));
        }

        msg.Add(container);
        go = false;
      }
    }
  }
  return msg;
}

And for the GUI implementation you just need some Layouting:

GUILayout.BeginHorizontal(GUI.skin.box, GUILayout.Height(100), GUILayout.Width(760), GUILayout.ExpandHeight(true));

GUILayout.Space(30);
GUILayout.BeginVertical();
  GUILayout.Space(10);
  GUILayout.FlexibleSpace();
  for (x in messageToReturn)
  {
    GUILayout.BeginHorizontal();
      for (n in x)
      {
        GUILayout.Space(10);
        GUILayout.Label(n[0], n[1]);
      }
      GUILayout.FlexibleSpace();
    GUILayout.EndHorizontal();
    GUILayout.Space(1);
  }
  GUILayout.Space(10);
  GUILayout.FlexibleSpace();
GUILayout.EndVertical();
GUILayout.Space(30);
GUILayout.FlexibleSpace();

GUILayout.EndHorizontal();

All the extra spaces and such I have in there for alignment reasons.

Now, there are some flaws to this system, most noticeably that if style changes font size some clipping may occur, there's nothing to handle words longer than the arbitrary line limit, and tags all have to be the same length (3 characters right now). These can be fixed with a little tweaking, but I figured it was complete enough to share with the community at large now.

Enjoy! If you have questions, feel free to email me at cjcurrie@blackstormsstudios.com