Posted on by

Some people attempt to create large scrolling blocks of text in Corona SDK only to find that it doesn’t work. The reason for this lies at Corona’s graphics core which is based on OpenGL. This means that almost everything on screen is a graphic… even text.

In native apps, text is just an object on the screen, but in OpenGL, everything must be an image “texture.” Thus, to make text appear on screen, Corona takes the string value that you pass to display.newText() and it tells the operating system’s font engine to generate an image using the font metrics that you provide. Then, a Corona display object (image texture) is returned and rendered on screen.

Why can this be a problem with very large blocks of text? Well in this case, you may encounter the device’s maxTextureSize limit. In OpenGL, the maximum texture size is defined as the greatest pixel limit — horizontal or vertical — that a rendered image can fit in. This includes large blocks of text, as noted, since they are rendered into image textures by Corona. For some older devices, this limit is as small as 1024 pixels in either direction, meaning that if you attempt to display a larger texture on these devices, OpenGL will be unable to render it properly.

Fortunately, most modern devices have a minimum texture size limit of 2048 pixels, and for your convenience, Corona provides this information via the following system.getInfo() call:

local maxTextureSize = system.getInfo( "maxTextureSize" )

Regarding Text

Even with a typical texture size limit of 2048 pixels, when creating large blocks of text via a multi-line display.newText() call, it’s easy to exceed the the limit. This may result in a solid white block where you expect the text to be. So how do you work around this issue? Simple — avoid creating extremely large blocks of text, and instead create smaller blocks (textures) and place them on the screen.

Understanding End-of-Line Encodings

Back in early computer history, there were two competing standards for defining the character sets used by computers: ASCII and EBCDIC. Similar to how BluRay won the battle against HD-DVD, ASCII ended up winning over EBCDIC. Now, almost everything, including OS X, Windows, Android, and iOS, are ASCII-based.

ASCII includes the 26 capital letters, 26 lowercase letters, numbers, and various symbol characters in a single byte of data. In addition to the visible characters that you know, ASCII includes a series of control characters. In fact, the first 31 characters are control characters. These control characters can be used to control text positioning on the screen and their heritage goes back to old manual typewriters. Some of these include:

  • CTRL-G — bell (character #7)
  • CTRL-H — backspace (character #8)
  • CTRL-I — tab (character #9)
  • CTRL-J — line feed (character #10)
  • CTRL-M — carriage return (character #13)

For those of us old enough to remember manual typewriters, a carriage return would return the typing head to the left side of the page. By itself, this would not advance the paper to the next line, so if you started typing after a carriage return, you’d overwrite the previous line. Thus, a second action called a line feed was needed to advance the paper a line.

Computers tried to mimic this system, however different operating systems approached it slightly differently. Microsoft DOS (and now Windows) opted for a 2-character end of line sequence, mimicking the typewriter. That is, each line ended with a CTRL-M, CTRL-J sequence. Unix, however, being the “minimalist” OS, used a line feed (CTRL-J) to indicate the end of line. Ironically, early Macs running OS versions 9 and earlier used a single carriage return (CTRL-M) as its end of line which made interoperability between the OSes a bit challenging.

Today, Android, OS X, and iOS are all based on Unix, so the more universal standard is to use a single CTRL-J line feed to mark the end of line. Windows, however, still uses the CTRL-M + CTRL-J combination. To add to the confusion, there are different ways that people reference these strings. For instance:

  • CTRL-M = ^M = \r
  • CTRL-J = ^J = \n

When you see the carrot (^) it means Control (CTRL). When you see a backslash (\) it indicates an escape character, so in this case, \r is return and \n is newline.

Multi-Line Strings in Corona

Because the operating systems for our mobile devices are Unix-based, the escape versions are more commonly used. You may have seen examples of multi-line strings in Corona code such as:

local myString = "First line.\nSecond line.\nThird line."

Specifying multiple lines in this manner will be treated as separate visual lines of text in Corona, assuming you define the width parameter in the display.newText() call (this tells Corona to render the text as a multi-line block instead of rendering it as one long line).

Alternatively, you can use Lua’s multi-line text quotes to achieve the same thing:

local myString = [[First line.
Second line.
Third line.]]

In this case, you don’t need to specify the newline marker like in the first version.

Breaking up Long Text

Because it usually doesn’t take much text to create a rendered text image that exceeds 2048 pixels, you can loop over large strings of text and, as mentioned above, break it into smaller blocks which can be rendered properly by OpenGL and remain below the max texture size limit.

When doing so, one option is to split apart long blocks using our natural language concept of paragraphs. In this case, you’ll generally only need to deal with the Unix newline. For this tutorial, let’s use some text generated by http://www.lipsum.com/. These 5 paragraphs of text will be inserted into a widget.newScrollView(), so remember to include the widget library:

local widget = require( "widget" )

local myText = [[Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque semper mollis erat a interdum. Praesent tristique diam in nulla varius, nec aliquet mauris posuere. Suspendisse pretium risus lacus, commodo lacinia sapien dictum et. Sed non varius felis. Curabitur elementum tortor non libero pulvinar, at convallis lectus varius. Interdum et malesuada fames ac ante ipsum primis in faucibus. Curabitur sit amet nunc congue, molestie erat vel, facilisis turpis. Morbi vitae diam ligula. Suspendisse purus turpis, commodo in aliquam id, lobortis a sapien. Sed at libero porta, aliquam odio nec, porta dui. In a congue velit. Aliquam ac quam feugiat, ultricies metus nec, porta neque. Phasellus posuere mollis magna, ac vestibulum ligula congue id. Pellentesque imperdiet aliquam lacus, ac pellentesque dui eleifend nec. Suspendisse auctor vehicula facilisis. Pellentesque id massa tincidunt neque luctus varius.

Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Maecenas sit amet dapibus nulla. Suspendisse ut risus nulla. Maecenas varius elit non faucibus fermentum. Fusce rhoncus, nisl et varius tristique, enim felis egestas purus, et feugiat lorem urna a augue. Maecenas non pulvinar tortor. Aenean condimentum nibh id eros fringilla viverra. Fusce condimentum urna ut volutpat porttitor. Nunc tincidunt congue ligula.

Duis placerat felis varius, convallis massa sed, volutpat magna. Sed vitae viverra neque. Integer ac sollicitudin libero, at ornare purus. Aliquam egestas hendrerit tellus. Aliquam eu elit vitae lorem lacinia tempus. Proin vel dictum mi. Maecenas porttitor, justo a dictum volutpat, nisl libero dictum ligula, vitae posuere urna elit a quam. Nam arcu metus, semper suscipit pellentesque ac, tempor ut arcu. Vestibulum eu nibh erat. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed semper sollicitudin lorem, vel commodo libero commodo eget. Proin lacinia euismod elit vitae porttitor. Proin ipsum neque, dictum at dictum eu, egestas malesuada turpis. Nulla eros lectus, adipiscing eget velit sed, malesuada aliquam ipsum. Curabitur et egestas massa. Vestibulum luctus est est, tincidunt viverra nisi vulputate id.

Integer lobortis tellus eu ligula viverra egestas. Quisque commodo, massa vel pretium imperdiet, nisl enim euismod justo, sed ultricies lacus mi ut nisi. Maecenas molestie vitae magna non interdum. In gravida ornare orci in vulputate. Praesent suscipit lobortis dui ut interdum. Proin pulvinar metus ligula, a malesuada nunc interdum at. Aenean et scelerisque enim. Integer eget congue sapien. Etiam suscipit mauris neque, id semper quam volutpat vel. Proin venenatis dictum felis quis ultricies. Suspendisse feugiat mi congue ante gravida, id accumsan leo mollis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In hac habitasse platea dictumst. Nulla facilisi.

Nam arcu mauris, convallis sit amet dictum consequat, imperdiet at mi. Vestibulum velit erat, accumsan sit amet vehicula vitae, tempor id nisi. Quisque eu tellus vulputate nisi vestibulum tincidunt at vitae tellus. Quisque sed pretium nisl. Vivamus a aliquet purus. Integer pulvinar neque in dapibus pharetra. Quisque convallis urna vulputate ligula mattis dictum. Vivamus pharetra molestie nunc, ac rhoncus dolor euismod at. Cras fringilla sollicitudin sapien vel sagittis. Donec dignissim scelerisque mi nec pulvinar. Mauris at metus gravida, lacinia dolor quis, vehicula lacus. Donec a pellentesque tellus. Praesent sit amet lorem nisl. Pellentesque interdum felis quis vehicula vestibulum. Donec ut dolor tortor.
]]

local paragraphs = {}
local paragraph
local tmpString = myText

local scrollView = widget.newScrollView
{
    top = 0,
    left = 0,
    width = display.contentWidth,
    height = display.contentHeight,
    scrollWidth = display.contentWidth,
    scrollHeight = 8000
}

local options = {
    text = "",
    width = 300,
    font = "HelveticaNeue",
    fontSize = 12,
    align = "left"
}

local yOffset = 10

repeat
    paragraph, tmpString = string.match( tmpString, "([^\n]*)\n(.*)" )
    options.text = paragraph
    paragraphs[#paragraphs+1] = display.newText( options )
    paragraphs[#paragraphs].anchorX = 0
    paragraphs[#paragraphs].anchorY = 0
    paragraphs[#paragraphs].x = 10
    paragraphs[#paragraphs].y = yOffset
    paragraphs[#paragraphs]:setFillColor( 0 )
    scrollView:insert( paragraphs[#paragraphs] )
    yOffset = yOffset + paragraphs[#paragraphs].height
    print( #paragraphs, paragraph )
until tmpString == nil or string.len( tmpString ) == 0

Let’s inspect this code in detail:

  • The paragraphs table is meant to hold the different display.newText() objects for each paragraph (the separated blocks). The paragraph and tmpString variables hold, respectively, the current paragraph text and a copy of the string with the current paragraph removed. You’ll be modifying tmpString so leave the original source string alone.
  • Next we construct the scroll view widget. In this case, it occupies the whole screen and it has 8000 pixels of total scrollable height. The variable yOffset is used to position each paragraph, one after the next, with a value of 10 (pixels) to provide a little space before each new paragraph.
  • Using the “new” table-based options format for display.newText(), we define the width of the text block, the font, fontSize, and the text alignment. We begin with text as an empty string, but we’ll fill it in shortly.
  • The main work happens in the repeat until loop. Since we want to perform this process at least once, for multiple paragraphs, a repeat loop makes more sense than a while loop. This loop will run until either tmpString is nil or the length of the tmpString is 0. This is an important check which will be covered in more detail below.
  • Inside the loop, we use string.match() to search the string for a newline characters (\n). This cryptic looking search string basically tells Lua to capture the string in two parts: any number of characters that are not a newline ([^\n*]) up until the first newline that we encounter (\n). Then, the rest of the string is captured into the second variable. These get returned as paragraph and tmpString.
  • With the variable paragraph now holding a single paragraph of text, we change the options table’s .text member to hold the paragraph that we want to generate, then we create the display object using display.newText( options ). By using #paragraphs + 1 as the loop index, it will create a new table entry at the end of the paragraphs table. Afterwards, we just reference the last entry in this table by using #paragraphs as the index.
  • Next, we position the text. To keep things simple, we change the anchor point for each text block to the top and left of the display object (0,0), then we position the x coordinate at 10 to provide some left padding. We set the y coordinate to yOffset, which for the first pass is also 10. The scroll view defaults to a white background, so we additionally change the text color to black (or any color of your choice) and, finally, we insert the paragraph into the scroll view.
  • On the following line, we increment yOffset by the height of the previous paragraph created. This will let you position the next block of text immediately below the previous one, providing the illusion that it’s one long block of text with a slight space between each paragraph.

Caution

In Lua, strings inside double quotes ("") can exist only on one line of physical code. It can be quite long, but it’s still one physical line so you’ll have to use \n to identify where the lines break. Alternatively, if you use the ([[ ]]) method of declaring a string, your text can be on multiple lines of code with an implied \n at the end of each line. Because of this, there’s a subtle difference in handling how string.match() fills out tmpString on the last iteration through. The double test of “is tmpString == nil or is it out of characters?” handles this condition, so both declaration styles will work.

Conclusion

Hopefully this tutorial has shown you how to construct long text blocks in Corona with just a little extra effort to work around the limits of maxTextureSize. Questions or comments? Please contribute below.


Posted by . Thanks for reading...

3 Responses to “Tutorial: Working With Large Blocks of Text”

  1. Kerem

    Rob, thank you very much for this tutorial. Very timely.

    For some reason the string.match did not work well with my data. I used a slightly less refined way of getting the same result. Sharing below. Not sure why string.match is not working well but this approach is. Will poke around some more to understand this better.

    Also note I’m looking for “\r\n” to avoid one extra line inserted in between my paragraphs. In other words, when I looked for \n to define paragraphs I ended up with extra lines in between my actual paragraphs. Changing from “\n” to “\r\n” helped solve that problem.

    repeat
    local b, e = string.find(tmpString, “\r\n”)
    if b then
    print(b .. ” ” .. e)
    paragraph = string.sub(tmpString, 1, b-1)
    tmpString = string.sub(tmpString, e+1)
    else
    paragraph = tmpString
    tmpString = “”
    end
    print(yOffset .. ” ” .. paragraph)
    options.text = paragraph
    paragraphs[#paragraphs+1] = display.newText( options )
    paragraphs[#paragraphs].anchorX = 0
    paragraphs[#paragraphs].anchorY = 0
    paragraphs[#paragraphs].x = 10
    paragraphs[#paragraphs].y = yOffset
    paragraphs[#paragraphs]:setFillColor( 0 )
    scrollView:insert( paragraphs[#paragraphs] )
    yOffset = yOffset + paragraphs[#paragraphs].height
    print( #paragraphs, paragraph )

    until tmpString == nil or string.len( tmpString ) == 0

    Reply
  2. Personalnadir

    In the past I’ve just written a webpage to a temporary file and loaded it in a webview. That provides a neat way of handling large blocks of text, and adds more advanced support for styling. Webviews do flash unappealing grey when loading an image, at least with backgrounds turned of, and handling touch events on them is tricky to say the least (but not impossible using some JavaScript and a URLRequest listener).

    Reply

Leave a Reply

  • (Will Not Be Published)