Skip to main content

Displaying text

All text, whether single or multi-lined, is drawn directly to a Canvas using the text and wrapped methods. Text rendering is also explicit, with each API requiring a position within the Canvas and the Paint used for filling. The following View draw's "hello" at 0,0 using the default font. But it is possible to change the font, letter, word and line spacing (for multi-lined text) as well.

package rendering import io.nacular.doodle.core.view import io.nacular.doodle.drawing.Color.Companion.Black import io.nacular.doodle.drawing.text import io.nacular.doodle.geometry.Point.Companion.Origin fun text() { val textView = view { //sampleStart render = { text("hello", Origin, color = Black) } //sampleEnd } }

You can draw wrapped text using wrapped, which takes information about the width you'd like the text to occupy. Wrapped text also allows you to specify the line spacing, otherwise, it shares the same inputs as regular text.

package io.nacular.doodle.docs.apps import io.nacular.doodle.application.Application import io.nacular.doodle.core.Display import io.nacular.doodle.core.view import io.nacular.doodle.docs.utils.controlBackgroundColor import io.nacular.doodle.drawing.Color.Companion.Black import io.nacular.doodle.drawing.Color.Companion.White import io.nacular.doodle.drawing.paint import io.nacular.doodle.drawing.width import io.nacular.doodle.geometry.Point.Companion.Origin import io.nacular.doodle.geometry.Size import io.nacular.doodle.layout.constraints.constrain import io.nacular.doodle.text.TextSpacing import io.nacular.doodle.utils.Resizer class MultiLinedTextApp(display: Display): Application { init { display += view { //sampleStart val text = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. It has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book." render = { rect(bounds.atOrigin, fill = White.paint) wrapped( text = text, at = Origin, width = this.width, fill = Black.paint, textSpacing = TextSpacing(wordSpacing = 5.0, letterSpacing = 1.0), lineSpacing = 1.2f ) } //sampleEnd }.apply { size = Size(400, 200) Resizer(this).apply { movable = false } } display.fill(controlBackgroundColor.paint) display.layout = constrain(display.first()) { it.width lessEq parent.width - 20 it.center eq parent.center } } override fun shutdown() {} }

Styled text

You can also draw text (single and multi-line) that is styled using the StyledText class and its DSLs.

package io.nacular.doodle.docs.apps import io.nacular.doodle.application.Application import io.nacular.doodle.core.Display import io.nacular.doodle.core.view import io.nacular.doodle.docs.utils.controlBackgroundColor import io.nacular.doodle.drawing.Color.Companion.Red import io.nacular.doodle.drawing.Color.Companion.White import io.nacular.doodle.drawing.Color.Companion.Yellow import io.nacular.doodle.drawing.FontLoader import io.nacular.doodle.drawing.paint import io.nacular.doodle.drawing.width import io.nacular.doodle.geometry.Point.Companion.Origin import io.nacular.doodle.geometry.Size import io.nacular.doodle.layout.constraints.constrain import io.nacular.doodle.text.Target.Background import io.nacular.doodle.text.TextDecoration import io.nacular.doodle.text.TextDecoration.Line.Under import io.nacular.doodle.text.TextDecoration.Style.Wavy import io.nacular.doodle.text.TextDecoration.Thickness.Absolute import io.nacular.doodle.text.TextSpacing import io.nacular.doodle.text.invoke import io.nacular.doodle.utils.Resizer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch class StyledTextApp(display: Display, fonts: FontLoader, appScope: CoroutineScope): Application { init { appScope.launch { val bold = fonts { family = "verdana" weight = 700 } display += view { //sampleStart val decoration = TextDecoration( lines = setOf(Under), color = Red, thickness = Absolute(1.0), style = Wavy ) val text = bold("Lorem Ipsum").." is simply "..Yellow("dummy text", target = Background).. " of the printing and typesetting industry. It has been the industry's standard dummy text ".. decoration("ever since the 1500s").. ", when an unknown printer took a galley of type and scrambled it to make a type specimen book." render = { rect(bounds.atOrigin, fill = White.paint) wrapped( text = text, at = Origin, width = this.width, textSpacing = TextSpacing(wordSpacing = 5.0, letterSpacing = 1.0), lineSpacing = 1.2f ) } //sampleEnd }.apply { size = Size(400, 200) Resizer(this).apply { movable = false } } display.fill(controlBackgroundColor.paint) display.layout = constrain(display.first()) { it.width lessEq parent.width - 20 it.center eq parent.center } } } override fun shutdown() {} }

Measuring Text

All text is positioned explicitly when rendered to a Canvas. This means text alignments like centering etc., require knowledge of the text's size. You can get this via a TextMetrics, which is available by default for injection into all apps.

This examples shows a View that draws some centered text based on the calculated size of the text.

package rendering import io.nacular.doodle.application.Application import io.nacular.doodle.application.application import io.nacular.doodle.core.Display import io.nacular.doodle.core.view import io.nacular.doodle.drawing.Color.Companion.Black import io.nacular.doodle.drawing.TextMetrics import io.nacular.doodle.drawing.paint import io.nacular.doodle.geometry.Point import org.kodein.di.instance class MyApp(display: Display, textMetrics: TextMetrics): Application { init { //sampleStart display += view { val hello = "hello" val textSize = textMetrics.size(hello) // cache text size render = { text( text = hello, at = Point((width - textSize.width) / 2, (height - textSize.height) / 2), fill = Black.paint ) } } //sampleEnd } override fun shutdown() {} } fun launch() { application { // TextMetrics is available to inject by default MyApp(display = instance(), textMetrics = instance()) } }
tip

The text location could be computed only when the View's size changes, since render can be called even more frequently than that.

Fonts

You can specify a font when drawing text or have Doodle fallback to the default. Fonts can be tricky, since they may not be present on the system at render time. This presents a race-condition for drawing text, since any text drawn with a Font that is simultaneously being loaded (or missing) can be shown in the wrong Font. This is what the FontLoader is designed to help with.

It presents an asynchronous API for fetching Fonts so the app is explicitly made to deal with this.

Module Required

You must include the FontModule in your application in order to use these features.

package rendering import io.nacular.doodle.application.Application import io.nacular.doodle.application.Modules.Companion.FontModule import io.nacular.doodle.application.application import io.nacular.doodle.core.Display import io.nacular.doodle.drawing.FontLoader import org.kodein.di.instance class FontLoaderApp(display: Display, fonts: FontLoader): Application { override fun shutdown() {} } fun main() { //sampleStart application(modules = listOf(FontModule)) { FontLoaderApp(display = instance(), fonts = instance()) } //sampleEnd }

Doodle uses opt-in modules like this to improve bundle size.

System Fonts

You can use FontLoader to check the system asynchronously for a given font. This allows you to check for OS fonts, or fonts that have been loaded previously.

package rendering import io.nacular.doodle.drawing.Font import io.nacular.doodle.drawing.FontLoader import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope suspend fun systemFont(scope: CoroutineScope, fonts: FontLoader) = coroutineScope { //sampleStart // launch a new coroutine for async font lookup val font: Deferred<Font?> = async { fonts { family = "Roboto" size = 14 weight = 400 } } //... font.await() //sampleEnd }

Font Files

You can also load a font from a file or url using FontLoader. This is similar to finding a loaded font, but it takes a font file url.

package rendering import io.nacular.doodle.drawing.Font import io.nacular.doodle.drawing.FontLoader import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope suspend fun fontUrl(fonts: FontLoader) = coroutineScope { //sampleStart async { // Load this front from the file at "urlToFont" val font: Font? = fonts("urlToFont") { family = "Roboto" size = 14 weight = 400 } } //sampleEnd }

Handling Timeouts

The FontLoader uses Kotlin's suspend functions for its async methods. Coroutines are a flexible way of dealing with async/await semantics. You can support timeouts using launch and canceling the resulting Job after some duration.

package rendering import io.nacular.doodle.drawing.FontLoader import io.nacular.doodle.scheduler.Scheduler import io.nacular.measured.units.Time.Companion.seconds import io.nacular.measured.units.times import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch suspend fun fontTimeout(fonts: FontLoader, scheduler: Scheduler) = coroutineScope { //sampleStart // track loading job val fontJob = launch { // assigns the font when the job resolves val font = fonts { family = "Roboto" size = 14 weight = 400 } } // Cancel the job after 5 seconds scheduler.after(5 * seconds) { if (!fontJob.isCancelled) { fontJob.cancel() } } //sampleEnd }