FancyLabel - Multicolor and Multifont label

Hey guys,

I needed a multi-color, multi-font label for my game Dungeon Sketch (http://forum.unity3d.com/viewtopic.php?t=9808). So I threw together a custom control this morning to do just that. I know this feature has been asked for by some people (me being one of them), so I thought I’d share it with you guys. Let me know if it works for you.

With this control you can draw a multi-line label that:

  • Can use multiple colors to highlight words.
  • Can use multiple fonts to accent words (ex. normal, bold, italic)
  • Can align the resulting multi-line fancy text to left, right or center.

To use the function you simply mark up your text strings with the proper escape sequences, which are:

#aabbccdd - Change current color to the one specified by the hex string red green blue alpha (much like HTML color codes, but also with an alpha channel)
#! - Revert back to the original color that was used before this function call
#n - normal font
#x - bold font
#i - italic font

And then call FancyLabel() in OnGUI() passing the rect to draw the string in, the text, the fonts, and the text alignment.

As for how it works. The function parses the marked up text string and breaks it up into separate labels every time it encounters a color or font change. GUILayout is used to line labels up nicely. There is more to it (its a lengthy function as you can see), but thats the jist of it.

See below for an example.

C# code:

int HexStringToInt( string hexString ) {
	int value = 0;
	int digitValue = 1;
	hexString = hexString.ToUpper();
	char[] hexDigits = hexString.ToCharArray(0, hexString.Length);
	
	for ( int i = hexString.Length - 1; i >= 0; i-- ) {
		int digit = 0;
		if ( hexDigits[i] >= '0'  hexDigits[i] <= '9' ) {
			digit = hexDigits[i] - '0';
		} else if ( hexDigits[i] >= 'A'  hexDigits[i] <= 'F' ) {
			digit = hexDigits[i] - 'A' + 10;				
		} else {
			// Not a hex string
			return -1;
		}

		value += digit * digitValue;
		digitValue *= 16;
	}
	
	return value;
}

// Recognized color string sequences:
// #aabbccdd - Change current color to the one specified by the hex string
//             red green blue alpha
// #!		 - Revert back to the original color that was used before this function call
// #n		 - normal font
// #x		 - bold font
// #i		 - italic font
public void FancyLabel( Rect rect, string text,
						Font normalFont, Font boldFont, Font italicFont,
						TextAlignment alignment ) {
	int 	i1 = 0, i2 = 0;
	bool 	done = false;
	bool	newLine = false;
	Color	originalColor = GUI.contentColor;
	Color 	textColor = new Color( originalColor.r,
								   originalColor.g,
								   originalColor.b,
								   originalColor.a );
	
	bool	leftSpace = false, rightSpace = false, topSpace = false, bottomSpace = false;

	Font    defaultFont = GUI.skin.font;
	Font	newFont = null;
			
	GUIStyle fontStyle = new GUIStyle(testStyle);
	
	// Start with normal font
	if ( normalFont != null ) {
		fontStyle.font = normalFont;
	} else {
		fontStyle.font = defaultFont;			
	}

	// NOTE: Lowering this padding reduces the line spacing
	// May need to adjust per font
	fontStyle.padding.bottom = -5;
	
	GUILayout.BeginArea( rect );
	GUILayout.BeginVertical( GUILayout.ExpandHeight( true ),
							 GUILayout.Width( rect.height ),
							 GUILayout.MinWidth( rect.height ) );
	GUILayout.BeginHorizontal( GUILayout.ExpandWidth( true ),
							   GUILayout.Width( rect.width ),
							   GUILayout.MinWidth( rect.width ) );
	// Insert flexible space on the left if Center or Right aligned
	if ( alignment == TextAlignment.Right || alignment == TextAlignment.Center) {
		 GUILayout.FlexibleSpace();
	}
			
	while ( !done ) {

		int skipChars = 0;
		int firstEscape, firstDoubleEscape, firstNewline;
		
		firstEscape = text.IndexOf("#", i2);
		firstNewline = text.IndexOf("\n", i2);

		if ( firstEscape != -1  ( firstNewline == -1 || firstEscape < firstNewline ) ) {
			i1 = firstEscape;
		} else {
			i1 = firstNewline;
		}
		
		// We're at the end, set the index to the end of the
		// string and signal an end
		if ( i1 == -1 ) {
			i1 = text.Length - 1;
			done = true;
		}

		fontStyle.normal.textColor = textColor;
		if ( newFont != null ) {
			fontStyle.font = newFont;
			newFont = null;
		}
		
		// If the next character is # then we have a ## sequence
		// We want to point one of the # so advance the index by
		// one to include the first #
		if ( !done ) {
			if ( text.Substring( i1, 1 ) == "#" ) {
				if ( (text.Length - i1) >= 2  text.Substring(i1 + 1, 1) == "#" ) {
					skipChars = 2;
				}
				// Revert to original color sequence
				else if (  (text.Length - i1) >= 2  text.Substring(i1 + 1, 1) == "!" ) {
					textColor = new Color( originalColor.r,
										   originalColor.g,
										   originalColor.b,
										   originalColor.a );
					i1--;
					skipChars = 3;
				}
				// Set normal font
				else if (  (text.Length - i1) >= 2  text.Substring(i1 + 1, 1) == "n" ) {
					if ( normalFont != null ) {
						newFont = normalFont;
					} else {
						newFont = defaultFont;
					}
					i1--;
					skipChars = 3;
				}
				// Set bold font
				else if (  (text.Length - i1) >= 2  text.Substring(i1 + 1, 1) == "x" ) {
					if ( boldFont != null ) {
						newFont = boldFont;
					} else {
						newFont = defaultFont;
					}
					i1--;
					skipChars = 3;
				}
				// Set italic font
				else if (  (text.Length - i1) >= 2  text.Substring(i1 + 1, 1) == "i" ) {
					if ( italicFont != null ) {
						newFont = italicFont;
					} else {
						newFont = defaultFont;
					}
					i1--;
					skipChars = 3;
				}
				//  New color sequence
				else if ( (text.Length - i1) >= 10 ) { 
					string rText = text.Substring( i1 + 1, 2 );
					string gText = text.Substring( i1 + 3, 2 );
					string bText = text.Substring( i1 + 5, 2 );
					string aText = text.Substring( i1 + 7, 2 );
			
					float r = HexStringToInt( rText ) / 255.0f;
					float g = HexStringToInt( gText ) / 255.0f;
					float b = HexStringToInt( bText ) / 255.0f;
					float a = HexStringToInt( aText ) / 255.0f;
					
					if ( r < 0 || g < 0 || b < 0 || a < 0 ) {
						Debug.Log("Invalid color sequence");
						return;
					}
					
					textColor = new Color( r, g, b, a );
					skipChars = 10;
					// Move back one character so that we don't print the #
					i1--;
				} else {
					Debug.Log("Invalid # escape sequence");
					return;
				}
			} else if ( (text.Length - i1) >= 1  text.Substring( i1, 1 ) == "\n" ) {
				newLine = true;
				i1--;
				skipChars = 2;
			} else {
				Debug.Log("Invalid escape sequence");
				return;
			}
		}
		
		string textPiece = text.Substring( i2, i1 - i2 + 1 );			
		GUILayout.Label( textPiece, fontStyle );
		
		// Unity seems to cut off the trailing spaces in the label, he have
		// to add them manually here
		// Figure out how many trailing spaces there are
		int spaces = textPiece.Length - textPiece.TrimEnd(' ').Length;
		
		// NOTE: Add the proper amount of gap for trailing spaces.
		// the length of space is a questimate here,
		// may need to be adjusted for different fonts
		GUILayout.Space( spaces * 5.0f );
		
		if ( newLine ) {
			// Create a new line by ending the horizontal layout
			if ( alignment == TextAlignment.Left || alignment == TextAlignment.Center) {
				GUILayout.FlexibleSpace();
			}
			GUILayout.EndHorizontal();
			GUILayout.BeginHorizontal( GUILayout.ExpandWidth( true ),
									   GUILayout.Width( rect.width ),
									   GUILayout.MinWidth( rect.width ) );			
			if ( alignment == TextAlignment.Right || alignment == TextAlignment.Center) {
				GUILayout.FlexibleSpace();
			}
			newLine = false;
		}
		
		// Store the last index
		i2 = i1 + skipChars;
	}
	if ( alignment == TextAlignment.Left || alignment == TextAlignment.Center) {
		GUILayout.FlexibleSpace();
	}
	GUILayout.EndHorizontal();
	GUILayout.FlexibleSpace();
	GUILayout.EndVertical();
	GUILayout.EndArea();		
}

To accomplish the text seen in this screenshot from Dungeon Sketch I simply used the FancyLabel like this:

		string text = "";
		text += "#FFDDDDFF"; // Light red
		text += "Fireball";
		text += "#!"; // revert to default color
		text += "\n\nHurls a ball of fire that ";
		text += "#x"; // bold font
		text += "explodes";
		text += "#n"; // normal font
		text += " on contact\n";
		text +="and damages all nearby enemies.\n\n";
		text += "#FF6666FF"; // red
		text += "#x"; // bold font
		text += "8";
		text += "#!"; // revert to default color
		text += "#n"; // normal font
		text += " to ";
		text += "#FF6666FF"; // red
		text += "#x"; // bold font
		text += "12";
		text += "#n"; // normal font
		text += "#!"; // revert to default color
		text += "#i"; // italic font
		text += " fire";
		text += "#n"; // normal font
		text += " damage";

		Globals.gGUIGlobal().FancyLabel( labelRect,
								  	     text,
								 	     Globals.gGUIGlobal().defaultFont,								 	     Globals.gGUIGlobal().defaultBoldFont,
								 	     Globals.gGUIGlobal().defaultItalicFont,
								 		 TextAlignment.Center);

(I spaced the string out like this so I could comment on what is happening, in practice this whole string can be easily written in one or two lines of code)

68597--2578--$fancylabel_137.jpg

Very very cool!
I made a very crude HTML parser the other day to make just that, but didn’t go nearly as far (didn’t do italic and bold for example).
Maybe we could merge the 2 solutions so you could input pseudo-HTML instead of your #stuff… someday.

Wow, that’s really cool! And it also shows how powerful UnityGUI 2.0 is!!! :slight_smile:

I get 2 errors when your script compiles,
A namespace can only contain types and namespace declarations
on lines 1 and 32.

Other than that it looks amazing! This is something I have want for a long time but dont have the coding ability yet. Thank you so much Dafu!
-Luke

Luke,

You’ll have to put the two functions inside some class. They can’t be sitting alone in the source code file.

I’ll look into throwing together a stand alone example project that uses the FancyLabel.

Nice. You should toss this at the Unify Wiki - its a nice place to have stuff like this.

Awsome! Got it to work, Im a javascripter myself so c# stuff is just close enough that I can understand whats happening, but just dissimilar enough that I miss much. Is there an easy way to find the aabbccdd of a given color?
Awsome Script
-Luke

Luke,

Yes there is an easy way to find the code.

The code is in the hex RGBA format.

If you use Photoshop the color picker will give you the first 6 hex digits, I think it’s refered to as HTML color or such.

You’re probably used to creating colors in Unity, so here is how you could translate between the two by hand:

Color myColor = new Color( 0.8f, 0.05f, 0.3f, 1.0f );

This color is:
0.8f Red (R)
0.05f Green (G)
0.3f Blue (B)
1.0f Alpha (A)

These values are in the range of 0.0f to 1.0f, the hex values in the hex color string that FancyLabel uses are in range 0 to 255 decimal, or 0 to FF hex. So to translate that color into a hex string do some simple math:

First figure out the decimal values in 0 to 255 range
0.8f * 255 = 204
0.05f * 255 = 12.75
0.3f * 255 = 76.5
1.0f * 255 = 255

Then convert them to hex (any calculator can do this for you)
204 → CC
12.75 → 0C (ignore fractions, and pad with a 0 if its a single digit)
76.5 → 4C
255 → FF

Put them together and you have:
#CC0C4CFF

That’s a rather long-winded explanation:)

Awesome Dafu, just what I was looking for; thanks for sharing!

This is so cool. Did you get any further with this adding new features or anything.
Good work though, thank you.

Any ideas how to add word wrapping to this? I’ve turned it on for the style used but then if say for example a single bold word follows a word wrapped normal font line the bolded word gets placed next to the normal line on the first line of text and not after the end of the word wrapped lines, thus making the text unreadable.

91621--3593--$picture_1_746.png

Dafu, thanks for sharing!
Very useful).

When I try using your code with Arial 16t font the word ‘explodes’ gets cut down to ‘explode’.

Any idea why this might happen? :shock:

Anytips for us Javascripters? I tried created a separate c# file and pasting in the code but no matter how many congigurations I tried I just kept getting errors. More to do with my terrible knowledge of C# than anything else though.

Great!!

Get it,Thanks. :stuck_out_tongue:

I have the same problem.
maybe the size is of label is not suitable.

was there any update on this for being put into javascript?

@onedong/Grimmy
You can call this function from JavaScript if you put the code in the standard assets folder. The reason for this is because scripts in the standard assets folder get compiled before scripts in folders created by you. Just instantiate the object you put the functions in, and call the function with the object in your OnGUI function.

@wisly/gnoblin
The reason it gets cut down is because the size of the Rect you passed is not big enough.

@monark
I also needed a version of this with word wrapping as part of the research project I’m working on (don’t ask), so I went ahead and wrote one:

//**************************************************************************
//
// Project Name: FancyLabel2
//
// Purpose: To allow Unity users to create GUI elements that can change
//          style partway through. Modified from another script to
//          include word wrap.
//
// URL of original program: [url]http://forum.unity3d.com/viewtopic.php?t=10175&start=0&postdays=0&postorder=asc&highlight=[/url]
//
// Notes: There is a variable called avgPpC (AVErage Pixels Per Character)
//        that needs to be changed to suit your font size/style.
//
// Author: Nathan Andre
//
// Email: na203602@ohio.edu
//
//**************************************************************************


using UnityEngine;
using System.Collections;

public class atextscript : MonoBehaviour {
	
	public GUISkin skin;

	// Use this for initialization
	void Start()
	{
	
	}
	
	// Update is called once per frame
	void Update()
	{
	
	}
	
	int HexStringToInt(string hexString)
	{ 
		int value = 0; 
		int digitValue = 1; 
		hexString = hexString.ToUpper(); 
		char[] hexDigits = hexString.ToCharArray(0, hexString.Length); 
    
		for(int i = hexString.Length - 1; i >= 0; i--)
		{ 
			int digit = 0; 
			if (hexDigits[i] >= '0'  hexDigits[i] <= '9')
			{ 
				digit = hexDigits[i] - '0'; 
			}
			else if(hexDigits[i] >= 'A'  hexDigits[i] <= 'F')
			{ 
				digit = hexDigits[i] - 'A' + 10;             
			}
			else
			{ 
				// Not a hex string 
				return -1; 
			}
			
			value += digit * digitValue; 
			digitValue *= 16; 
		} 
	
		return value; 
	} 



public void FancyLabel2(Rect rect, string text, Font normalFont, Font boldFont, Font italicFont, TextAlignment alignment)
{
	//bool   leftSpace = false, rightSpace = false, topSpace = false, bottomSpace = false; 

	Color    textColor = GUI.skin.GetStyle("Label").normal.textColor; 
	Font    defaultFont = GUI.skin.font;
	Font   newFont = null; 

	//GUIStyle fontStyle = new GUIStyle(testStyle); 
	GUIStyle fontStyle = new GUIStyle();
	fontStyle.normal.textColor = textColor;
    
	// Start with normal font 
	if(normalFont != null)
	{ 
		fontStyle.font = normalFont; 
	}
	else
	{ 
		fontStyle.font = defaultFont;          
	}

	// NOTE: Lowering this padding reduces the line spacing 
	// May need to adjust per font 
	fontStyle.padding.bottom = -5; 
    
	GUILayout.BeginArea(rect); 
	GUILayout.BeginVertical(GUILayout.ExpandHeight(true), 
							GUILayout.Width(rect.height), 
							GUILayout.MinWidth(rect.height)); 
	GUILayout.BeginHorizontal(GUILayout.ExpandWidth(true), 
								GUILayout.Width(rect.width), 
								GUILayout.MinWidth(rect.width));
								
	// Insert flexible space on the left if Center or Right aligned 
	if(alignment == TextAlignment.Right || alignment == TextAlignment.Center)
	{ 
		GUILayout.FlexibleSpace(); 
	}
   
    int newline = 0;
    bool clearBuffer = false;
	double pixelsPerLine = 0;
	double avgPpC = 8.5;
	
	text = text.Replace("\n", "");
	text = text.Replace("\r", "");
	
	string[] toks = text.Split(' ');
	string output = "";

	for(int i=0; i<toks.Length; ++i)
	{
		//Add a leading space if you need one
		if(i != 0)
		{
			output += " ";
			pixelsPerLine += avgPpC;
		}
				
		int index = 0;
		while(index < toks[i].Length)
		{
			if(toks[i][index] == '\\')
			{
				//Must be an escape character
				if(toks[i][index+1] == 'n')
				{
					++newline;
				}
				else if(toks[i][index+1] == '#')
				{
					output += "#";
				}
				
				index += 2;
			}
			else if(toks[i][index] == '#')
			{
				//Must be a style sequence
				//The style will probably change, so might as well start a new label
				clearBuffer = true;
				
				if(toks[i][index+1] == '!')
				{
					//Original Color
					textColor = GUI.skin.GetStyle("Label").normal.textColor;
				}
				else if(toks[i][index+1] == 'n')
				{
					//Normal Font
					if (normalFont != null)
						newFont = normalFont;
				}
				else if(toks[i][index+1] == 'x')
				{
					//Bold Font
					if (boldFont != null)
						newFont = boldFont;
				}
				else if(toks[i][index+1] == 'i')
				{
					//Italic Font
					if (italicFont != null)
						newFont = italicFont;
				}
				else
				{
					//Must be a color change
					string rText = toks[i].Substring(index + 1, 2);
					string gText = toks[i].Substring(index + 3, 2);
					string bText = toks[i].Substring(index + 5, 2);
					string aText = toks[i].Substring(index + 7, 2);

					float r = HexStringToInt(rText) / 255.0f; 
					float g = HexStringToInt(gText) / 255.0f; 
					float b = HexStringToInt(bText) / 255.0f; 
					float a = HexStringToInt(aText) / 255.0f; 
                
					if(r < 0 || g < 0 || b < 0 || a < 0)
					{ 
						Debug.Log("Invalid color sequence");
						return;
					} 
                
					textColor = new Color(r, g, b, a);
					index += 7;
				}

				index += 2;
			}
			else
			{
				//Must just be a regular string
				//Check to see if a new line is needed, then go ahead and add the text to the string
				int index2, firstFormat, firstEscape = 0;
				firstFormat = toks[i].IndexOf("#", index);
				firstEscape = toks[i].IndexOf("\\", index);
				//if(firstFormat != -1  (firstFormat < firstEscape  firstEscape != -1))
				if(firstFormat != -1  (firstEscape!=-1?firstFormat<firstEscape:true))
				{
					index2 = firstFormat;
				}
				else if(firstEscape != -1)
				{
					index2 = firstEscape;
				}
				else
				{
					index2 = toks[i].Length;
				}
				
				//Check to see if the words need to wrap
				if((pixelsPerLine + (index2 - index)*avgPpC) >= rect.width)
				{
					if(newline == 0) newline = 1;
				}
				
				//Check to see if you need to make a label
				if(clearBuffer || newline > 0)
				{
					//Clear the buffer if the style changes or there is a newline
					GUILayout.Label(output, fontStyle);

					//Add in trailing spaces
					int spaces = output.Length - output.TrimEnd(' ').Length;
					//Might have to adjust this constant
					GUILayout.Space(spaces * 5.0f);
					//And also count that space in the pixel size of the buffer...
					pixelsPerLine += spaces*avgPpC;
					
					//Clear the buffer and cleanup
					output = "";
					clearBuffer = false;
					fontStyle.normal.textColor = textColor;
					if(newFont != null)
					{ 
						fontStyle.font = newFont; 
						newFont = null; 
					}
				}

				//You might have multiple newlines since the last label was created
				//ie if you do multiple newlines in a row
				while(newline > 0)
				{
					//Create a new line by ending the horizontal layout 
					if(alignment == TextAlignment.Left || alignment == TextAlignment.Center)
					{ 
						GUILayout.FlexibleSpace(); 
					} 
					GUILayout.EndHorizontal(); 
					GUILayout.BeginHorizontal(GUILayout.ExpandWidth(true), 
												GUILayout.Width(rect.width), 
												GUILayout.MinWidth(rect.width));          
					if(alignment == TextAlignment.Right || alignment == TextAlignment.Center)
					{ 
						GUILayout.FlexibleSpace(); 
					}
					
					//You have to include this label, otherwise the newline will be
					//at the same place as the last one.
					if(newline > 1) GUILayout.Label(" ", fontStyle);
					
					--newline; 
					pixelsPerLine = 0;
				}
				
				//Write the new stuff to the buffer
				output += toks[i].Substring(index, index2-index);
				pixelsPerLine += (index2-index)*avgPpC;
				index += index2-index;
			}
		}
	}
	
	
	//Clear the buffer one last time
	GUILayout.Label(output, fontStyle );
	
	if(alignment == TextAlignment.Left || alignment == TextAlignment.Center)
	{ 
		GUILayout.FlexibleSpace(); 
	} 
	GUILayout.EndHorizontal(); 
	GUILayout.FlexibleSpace(); 
	GUILayout.EndVertical(); 
	GUILayout.EndArea();   
}

}

I have not done a lot of testing on this, but it works well enough for my purposes. If you try it out, shoot me a private message to tell me how it goes. One thing to mention is that it is a very crude form of word wrap. It doesn’t take the particular font/font size into account, so there is a variable you have to change called avgPpC (AVErage Pixels Per Character). Right now it is at 8.5, which is the value that works for my purposes.

EDIT 1: Changed the code a little bit. Did some testing and found a bug. Will fix tomorrow.
EDIT 2: Fixed the code to change fonts and do trailing spaces. Also added some comments.

Could you post a sample project?

I’m trying to reverse engineer this code, but I’m having a hard time figuring out what is going on.

At what point is the text drawn on screen, and is there a speed penalty for multiple font styles and colors?

I tried uploading a sample project as an attachment, but it wouldn’t let me for some reason… Any idea why? Is it possible that I don’t have the proper permissions?

Text is drawn to the screen any time you see a GUILayout.Label function call. In the comments I usually call this “clearing the buffer”. Sorry if that caused confusion.

Yes, there are performance penalties to using new colors, but it’s usually pretty insignificant unless you’re changing style every other word or something.[/quote]

You should be able to post a project if you archive it as a zip file and if it isn’t too large. A large project can often be stripped down to a minimal setup that displays the problem (eg, remove unused stuff from Standard Assets, etc).