In this tutorial, we will look at drawing multi-line text to the screen.
We will draw our multi-line text by iterating through each of the characters in the text, and keeping track of the width of the current line. When the width of the current line is exceeded, we will find the most recent space (if there is one) and we will drop down a line from that point. After deciding where the end of the line should be, we will draw said line. When the end of the final line of text is reached, we will draw this line to the screen.
When we come to draw multi-line text to the screen, we will want to call our drawSingleLineText function to draw each individual line to the screen. Since each individual line is not null termiated (each line is a part of a larger string - the inputted text to draw), we will need a means of specifying how many characters we want to draw. To do this, we will need to pass in a new variable, an integer called length. This will replace the local variable called "length" in the function. I will put this parameter after the charSize parameter, so that the line creating our function now looks like this: "void drawSingleLineText(int x, int y, char text[], int charSize, int length, uint8_t red, uint8_t green, uint8_t blue)".
When we want to specify a number of characters to draw, we should pass a positive integer into the length argument. When we want to draw an unknown number of characters (up to a null terminator), we should pass in 0.
Inside our function, we should replace the first line "int length = strlen(text);" with the following three lines: "if(length < 1){", "length = strlen(text);" and "};". These lines will check to see if an invalid length has been passed in. If so, we will set the length to the length of the string (until null termination). This means that we can pass 0 into the length parameter when we want to draw a single line of text of unknown length. The rest of our drawSingleLineText function should go unchanged.
Now we can create our drawMultiLineText function. This should be done in the render.c file, and the function should be declared in the main.h file. It should be a void function and take 8 parameters. The first two should be the x and y coordinates. These will mark the bottom left corner of the top line of text. I will name these "x" and "y". The next is a null terminated string. I will name this "text". Next is another integer: the character size in pixels. I will name this "charSize". After this is the maximum width of a single line of text. I will call this "maxWidth". This should also be an integer. The next three are all of the type uint8_t. They are the red, green and blue values of the colour the text will be drawn in. This function can be created with the following function: "void drawMultiLineText(int x, int y, char text[], int charSize, int maxWidth, uint8_t red, uint8_t green, uint8_t blue){".
Inside this function, we first want to check if maxWidth is smaller than charSize. If this is the case, then we will not be able to draw any text, as the line width is smaller than a single character. We should return out of the function if this is the case. We can do this with the if statement: "if(charSize > maxWidth){", with the line "return;" inside.
If maxWidth is at least the size of a single character, we can continue with our function. Next, we should calculate the length of our string. We should save it to an integer variable called length. This can be done with the line: "int length = strlen(text);". Next, we should create another integer variable called startIndex. This will always point to the first character of the current line as we iterate through all the characters. This should be initialised to 0. This can be done with the line: "int startIndex = 0;". After this, we should create another integer named "rowNumber". This will store the number of the current line/row (starting with the top line being row 0). This should also be initialised to 0. This can be done with the line: "int rowNumber = 0;". The next variable to create is an integer named "charactersPerRow". This stores the maximum number of characters in a row. This should be initialised to maxWidth / charSize. We can do this with the line: "int charactersPerRow = maxWidth / charSize;".
Next, we should iterate through each character in the row. We should first create a loop counter called "i". This should be done with the line: "int i;". We can loop through all the characters using the line: "for(i = 0; i < length; i++){".
Inside the loop we want to continuously iterate until the end of a line is reached. Then we want to check for the most recent space and drop down a line. We can check if the end of the current line has been reaced using the if statement: "if((i + 1 - startIndex) * charSize > maxWidth){". The number of the current character on the current line is equal to i - startIndex, since startIndex always points to the start of the current line. We add 1 to i - startIndex because we want to get the rightmost side of the current chraacter, and we multiply by charSize to get the width of the current line so far (including our current character). If the width of the current line will be greater than maxWidth when our character is added, we want to drop down a new line.
Inside the previously mentioned if statement, we want to write the code to drop down a new line. Before we drop down a new line, we want to find the most recent space character, so we can drop the current word down onto the next line (and we don't have words spanning multiple lines if we can help it). To do this, we need to create a variable called endIndex. It should be an integer and should be initialised to -1. The purpose of this variable is to point to the most recent space character on the current line. We can create this with the line "int endIndex = -1;". After this, we should create a loop counter called j. We will use this to check the previous characters on the current line. We should start at the current character. Therefore, we should set j to the value of i, with the line: "int j = i;".
Now we need to subtract j until a space is found at text[j], until j is smaller than 0 or until the difference between i and j is greater than the number of characters per line (this indicates that the previous line has been exceeded). If there is not a space in the current line, we just want to drop down the current character to the next line. This can be accomplished with the while loop: "while(j >= 0 && j > i - charactersPerRow){". Inside this loop, we want to check if the character text[j] is a space, which we can do with the line: "if(text[j] == ' '){". If a space has been found, we should set endIndex to j. We should then break out of the loop. This can be achieved using the lines "endIndex = j;" and "break;". After the space-checking if statement, we want to subtract 1 from j to move along to the next character. We can do this with the line: "j -= 1;".
After this while loop, we should check if endIndex is still equal to -1 using the if statement: "if(endIndex == -1){". This means that no space could be found. If this is the case, then we want our current character to be the start of the new line, and therefore the character before it (at i - 1) should be the last character on the line. Because of this, we should set endIndex to i - 1, which we can do with this line: "endIndex = i - 1;". Since we want to draw the current character on the next line, we want to subtract 1 from i so that our current character is added to the next line in the next iteration. We can do this with the line: "i -= 1;". If endIndex is not equal to -1, then it must be a valid index. Since we want to draw everything after endIndex on a separate line, we should set i to endIndex + 1, so that everything after the previous space gets added to the next line. We can do this with the line: "i = endIndex;".
After setting the values of endIndex and i, we finally want to display our line of text. This can be done with the line: "drawSingleLineText(x, y - rowNumber * (charSize * 1.25), text + startIndex, charSize, (endIndex + 1 - startIndex), red, green, blue);". We always want to start at the x coordinate passed into our drawMultiLineText function, as this is the coordinate of the left side of the text. The y will vary with each row. I want the vertical distance between the bottom corners of each row to be equal to 1.25 multiplied by the character size, so that there is always a neat gap between lines to make them easier to read. We want to subtract this value once from y for each row of characters we wish to draw, hence why the y coordinate passed in is y - rowNumber * 1.25 * charSize. You can use whatever value you would like in the place of the 1.25. We add the startIndex value to the text string so that the string always starts from the beginning of the current line when drawing. The length of the string is set to endIndex + 1 - startIndex. This is because we want to include the character at endIndex in our rendering. To better understand this, imagine there are two characters you want to read, at positions 0 and 1 in a string. The difference between the positions of these characters is equal to 1 - 0 = 1, even though there are two characters we want to read, not one. This is why we add 1 to endIndex - startIndex.
After drawing this line of text, we want to increment the row number. We can do this using the line: "rowNumber += 1;". We also want to set the start index to endIndex + 1, as this will be the first character in the new line. This can be done with the line: "startIndex = endIndex + 1;".
If the maxWidth value has not been exceeded on the current line (i.e. the "if((i + 1 - startIndex) * charSize > maxWidth){" if statement failed), we want to check if the end of the string has been reached. We can do this with the else if statement: "} else if(i == length - 1){". If this else if statement is true, then we should draw the final row of characters using the line of code: "drawSingleLineText(x, y - rowNumber * (charSize * 1.25), text + startIndex, charSize, (i + 1 - startIndex), red, green, blue);". Here, i is the final character of the text and of our line.
That completes our drawMultiLineText call. Now we simply have to run it inside our render function. I will do this with the line: "drawSingleLineText(x, y - rowNumber * (charSize * 1.25), text + startIndex, charSize, (i + 1 - startIndex), red, green, blue);".
That's all for the theory of this tutorial. As usual, the code for this tutorial is shown below.
I have only included the code for render.c here, as the code for main.c has not changed and the only addition to main.h is the modification of the drawSingleLineText declaration, and the addition of the drawMultiLineText declaration. As usual, I will be compiling this code with the MinGW compiler, using the command "gcc main.c -lgdi32 -o app".
render.c
Running our program should give the following output:
That's all for this tutorial. In the next tutorial, we will be looking at rotating a bitmap.