StyledText

StyledText is a built-in class for arranging and displaying text that contains a mixture of styles, such as different fonts and sizes. The text can also contain in-line images, which are laid as though they were part of the text.

This class is a higher-level alternative to the basic text drawing primitives that the Custom Drawing system provides. The basic text functions operate at the much simpler level of drawing individual strings, each in a single style. That works for simple graphics with plain text that's static enough that you can position and arrange it in advance, but it's impractical for text that uses a mixture of styles, since you'd have to do all of the alignment calculations and word-wrapping manually.

StyledText lets you construct a text stream containing a mixture of fonts and styles, and does all of the word-wrapping and alignment calculations necessary to lay out the text in a given screen area. You can then measure the space needed to display the whole block of text, and display the whole block with a single method call.

StyledText works with the Custom Drawing system, so you can combine it with the other drawing primitives to create complex layouts that incorporate styled text as well as other graphics. StyledText amounts to an extended version of the drawText() method in the custom drawing context.

You might also take a look at HtmlLayout, which is another way of handling mixed-style text. HtmlLayout has more extensive layout capabilities than StyledText that include most of the core HTML and CSS layout features. The interface to HtmlLayout is also rather different, in that you set it up using HTML/CSS source code, as though you were creating a Web page. StyledText has a more programming-oriented interface. Each approach is more convenient for certain kinds of tasks, so you can choose the one that works better for your use case.

How to use StyledText

The first step is to create a StyledText object to hold the mixed-style text you wish to display. Do this by using the Javascript new operator to create the object:

let s = new StyledText();

Optionally, you can pass an object to the constructor, to specify the visual styling for the text block. That lets you select the text alignment (left, centered, right, justified), set a background fill color for the whole block of text, and set the default font style to use for text within the block. See new StyledText() below for details.

A StyledText object doesn't need a Drawing Layer or Custom Drawing context. It's just a container for the styled text itself, so it's not directly tied to any particular window or drawing operation. You can create one at any time and hold onto it indefinitely, even when a drawing operation isn't in effect. Likewise, you can reuse the same StyledText object to draw the same text in different drawing contexts.

Once you've created a StyledText object, you add text to the layout by calling the add() method on the object:

s.add("Styled Text Demo");

Each bit of text you add is appended to any text you added previously, so you can build up the overall layout by adding each piece of text in turn until the text is complete.

When you pass a string to add(), it adds the text using the default font style. You can specify a specific font style to use as the default when creating the StyledText object with new.

But StyledText wouldn't be very different from the drawText() primitive if this was all you could do! The whole point of StyledText is to use a mixture of styles within one block of text. So each time you call add(), you can also optionally specify a specific style to use for just that one fragment of text being added. You do this by calling add() with a descriptor object instead of just a simple string:

s.add({ font: "Tahoma", size: 30, weight: 700, text: "Some big, bold text!" });

That adds the text in the specified font, size, and weight. Other properties you can set include style, to select italics, oblique, or regular text; underline and strikethrough, to set text decorations; color, to set the text color; backgroundColor, to set the background fill color for the string; and stretch, to set the horizontal width of the font, from condensed to extended. See the add() method for full details on what can go in the descriptor.

You don't have to specify all of those properties every time you add text. You just have to specify the ones that you want to customize for that particular string. Any of the style properties you include will override the defaults, and any that you leave out will use the defaults.

You can also "nest" styles within styles. The text property can itself be another descriptor, where you add more styles:

s.add({ font: "Tahoma", text: { size: 30, text: "Big Tahoma Text!" } });

That might not look all that useful: after all, you could have just put that all in one descriptor, right? Well, it turns out that there's yet an other option for text, which is that it can be an array of multiple strings and/or descriptors. That's where the nesting becomes really useful. It lets you activate a style for a series of text fragments, which can then add their own individual styles to the "parent" style:

s.add({ font: "Tahoma", size: 30, text: [ { weight: 700, text: "Bold!" }, { style: "italic", text: "Italics!!" }, { weight: 700, style: "italic", text: "Bold Italics!!!" } ] });

In the example above, all three text fragments are displayed in 30-point Tahoma, but the first is in boldface, the second is in italics, and the the third is in bold and italics.

You can continue that nesting to any depth. Just use another array-of-descriptors as the inner text property.

One little note about these examples: add() doesn't insert any spaces beyond what you explicitly tell it to, so all of the text we've been inserting will just run together. In actual use, you'll usually want to include spaces between adjacent strings, unless you actually want them to run together without any spacing.

Using images: You can include in-line images that are formatted along with the text. This is useful for things like icons and bullets that aren't part of a font. To add an image to the text flow, call add() with a descriptor that includes an image property instead of a text property. The image property lets you specify the file containing the image to display, and other properties to size and align the image.

s.add({ image: { src: "GameIcon.png" }});

Drawing the text: Once you've added all of the text to the StyledText object, you're ready to draw it. To do this, you use the standard Custom Drawing procedure. As always, you have to write a "drawing function" and pass it to the appropriate system function to initiate drawing, such as the draw() method on a DrawingLayer object. Your drawing function receives a drawing context object as its argument, and this object is needed to draw the StyledText layout.

Once you're in a custom drawing function and have a drawing context object available, drawing the StyledText is just a matter of calling its draw() method. The arguments to this method are the drawing context object, and the boundaries of the rectangular area within the window where you want to display the text: left coordinate, top coordinate, width, and height, all in pixels.

For example, this draws styled text in a Drawing Layer object, positioning it at the top left, and giving it full run of the available space:

myDrawingLayer.draw(dc => { let size = dc.getSize(); s.draw(dc, { x: 0, y: 0, width: size.width, height: size.height }); }, 640, 480);

Measuring the text: One of the key things StyledText can do for you is automatic word-wrapping, taking into account the different styles of the text and the bounds of the drawing area. You can measure the effect of word-wrapping using the measure() method of the StyledText object, specifying the width of the available space. That'll figure the word-wrapping break points and tell you (among other things) how much vertical space is needed to fit the resulting text. It'll also tell you the actual width needed, which might be a little different from the bounding-box width you specified, since the word breaks might fall in such a way that none of the lines fully fill out the available width (and, in some cases, text that can't be broken across lines might exceed the available width).

// measure the layout bounds with a width of 250 pixels let metrics = s.measure(250);

The measure() method returns an object with properties width and height giving the required display size of the text.

The returned object has another property, drawingArea, giving a rectangle indicating the bounding box of all of the pixels affected by the text display. This has properties x, y, width, and height. The width and height in drawingArea might be slightly different from the main width and height values returned, since the ones in drawingArea reflect the actual pixel area, as opposed to the text layout area.

In most cases, you can just ignore this drawingArea. The width and height usually tell you everything you need to know to arrange the text display on the screen. But the drawing area might be useful in some special cases, so it's included in case you need it; read on if you're curious.

The difference between the drawing area and the layout area is a technicality of the way computerized fonts are constructed. Computer fonts are laid out using rectangular "character cell" boxes. A given font's cells are always the same height, but they can vary in width - the cell for an "M" is usually wider than for an "i", for example. The cells are used to line up the glyphs on a line of text, and to space them out horizontally. The complication with "overflow" is that some glyphs aren't completely confined to their cells. Some fonts have glyphs that spill out slightly. This is especially common with decorative fonts, which often have "swashes" or ligatures that intentionally overlap with nearby characters and therefore have to spill outside of their cells a little bit. It's also common with italic and oblique typefaces, since the tilt often pushes a little bit of each glyph outside of its rectangular cell. The main width and height properties returned from measure() only cover the bounds of the area needed for all of the character cells. If any of the glyphs at the outside spill out of their cells, the drawingArea box will reflect the larger area needed to account for all of the pixels. Conversely, if all of the glyphs are not only confined to their character cells, but don't even reach the edges of their cells, drawingArea will reflect that by being slightly smaller than the rectangle implied by width and height.

The main use I can think of for the drawing area is clipping. If you need to clip the StyledText display to a limited area (to avoid having it overlay adjacent graphics, for example), the drawing area rectangle is important because it tells you the full pixel extent of the text. If you clip to the layout width and height, you might chop off a few pixels at the edges where glyphs extend slightly outside their cells.

Interaction with other drawing primitives: StyledText does its actual drawing using a Custom Drawing graphics context, just like all of the Custom Drawing functions, so you can freely combine it with the other drawing functions to build up composite graphics out of different elements. Drawing styled text simply draws pixels into the in-memory bitmap that the drawing function is constructing, just like drawing a simple text string via the drawing context's drawText() method.

In the absence of explicit background colors, StyledText uses a transparent background. This makes it easy to overlay styled text on top of other graphics. Draw the background material first, then draw the StyledText object to draw the text over the background.

StyledText doesn't affect of the text-related "status" elements in the drawing context. It doesn't use or affect the text origin, text bounding box, of any of the font, size, style, or color attributes in the drawing context. All of the style and positioning information in a StyledText object is self-contained.

Properties and methods

A StyledText object has the following properties and methods:

new StyledText(desc): Create a new StyledText object. desc is an optional descriptor object specifying the overall block-level style, and the default font styling for text added to the object. It has the following properties, all optional:

styledText.add(desc): Adds text to the layout. If desc is a string, the text in the string is simply added to the layout, using the default style. If desc is an array, each element of the array is added to the layout, one at a time, as though you had called add() with each element of the array individually. desc can also be a descriptor object, with the following properties, all of which are optional:

styledText.draw(drawingContext, layoutRect, clipRect): Draw the object's text (defined when it was created with new) into the drawing context, laying it out to fit the layout area given by layoutRect.

The drawingContext object is the argument that the system passes to your custom drawing function when you use a Drawing Layer or other Custom Drawing mechanism. As a result, you can only call the StyledText draw() method from within one a drawing function, since that's the only place where a drawing context is valid. (This restriction doesn't apply to other StyledText operations, though. You can create a StyledText object at any time, and you can hold onto it and use it in as many different drawing operations as you want. The StyledText object itself isn't tied to a particular window or drawing operation; it's just an encapsulation of the formatted text.)

The layout rectangle isn't a hard limit on the bounds of the displayed text - the display might exceed this area in some cases, such as when a word that can't be broken across lines is too wide to fit the available space. The formatter tries to fit the text to the specified area, but the text is allowed to spill outside the bounds if there's no way to make it fit.

The layout rectangle is also the reference frame for text alignment. If the object was created with center alignment, for example, the text will be centered within the layout rectangle. Likewise, right alignment will draw the text aligned to the right side of the layout rectangle.

If you specified any padding when creating the StyledText object, the padding is applied inside the layout rectangle. The results from measure() behave the same way, by including the padding in the size calculations returned. That means you can usually just ignore the padding and let the StyledText object handle it, since as far as you're concerned, it's just part of the text layout.

clipRect is optional. If it's included, the drawing will be "clipped" to this area, meaning that pixels outside of this area will simply not be drawn. Clipping doesn't attempt to keep whole characters intact, since it's simply at the pixel level; it can result in characters being chopped in half vertically or horizontally if they spill outside of the clipping boundaries. If clipRect is omitted (the argument is missing, or you explicitly pass undefined), the drawing isn't clipped, so it can affect pixels outside of the layout rectangle. The actual display area can exceed the layout area for a number of reasons, such as words that are too long to fit the specified layout width, too many lines to fit the layout height, or fonts with characters that are deliberately designed to extend outside of their character cells (which can happen with italic or oblique type, and is also common with decorative fonts, where "swashes" and ligatures and the like are often designed to extend far enough to overlap adjoining characters).

Both rectangles (layoutRect and clipRect) are specified as objects, with properties { x, y, width, height }, giving the upper left corner of the rectangle, and its size. As with all Custom Drawing coordinates, the rectangle coordinates are all in terms of pixel positions within the Custom Drawing surface, starting with (0,0) at the upper left corner of the drawing surface.

styledText.measure(width): Measure the text contained in the StyledText object, taking into account all of the font styles and sizes, and fitting it to the given width by word-wrapping the text. width is specified in pixels.

The method returns an object with the following properties: