Skip to main content

Text

Over on Shapes & Paths, you met Doodle's drawing primitives. Text plays by slightly different rules than the rest of them — it's positioned explicitly, and it's the one primitive where knowing its size ahead of time actually matters, since the font it needs might not even be loaded yet.

Drawing text​

Text is drawn with text, which always wants an explicit position alongside its Paint:

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 } }

Wrapping text​

wrapped draws multi-line text that wraps to a given width, with line spacing under your control too:

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 { suggestSize(Size(400, 200)) Resizer(this, movable = false) } display.fill(controlBackgroundColor.paint) display.layout = constrain(display.first()) { it.size prefers it.size.value it.width lessEq parent.width - 20 it.center eq parent.center } } override fun shutdown() {} }

Styled text​

For text where only parts need different styling — bold words, colored spans, underlines — build an AnnotatedString with its DSL and pass it to text or wrapped the same way:

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.DEFAULT_FONT_FAMILIES 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 { families = DEFAULT_FONT_FAMILIES 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(Background) { "dummy text" }.. " 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." //sampleEnd render = { rect(bounds.atOrigin, fill = White.paint) wrapped( text = text, at = Origin, width = width, textSpacing = TextSpacing(wordSpacing = 5.0, letterSpacing = 1.0), lineSpacing = 1.2f ) } }.apply { suggestSize(Size(400, 200)) Resizer(this, movable = false) } display.fill(controlBackgroundColor.paint) display.layout = constrain(display.first()) { it.size prefers it.size.value it.width lessEq parent.width - 20 it.center eq parent.center } } } override fun shutdown() {} }

Fonts​

Here's a wrinkle text alone doesn't have: the font it needs might not be loaded yet, or might not exist on the user's machine at all. So rather than assume it's just there, Doodle hands you a FontLoader with an async API for fetching fonts explicitly.

Send it after a system font, or load one from a file or URL:

package rendering import io.nacular.doodle.drawing.FontLoader import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope suspend fun systemFont(fonts: FontLoader) = coroutineScope { // launch a new coroutine for async font lookup //sampleStart val font = async { fonts { family = "Roboto"; size = 14; weight = 400 } } //sampleEnd //... font.await() } package rendering import io.nacular.doodle.drawing.FontLoader import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope suspend fun fontUrl(fonts: FontLoader) = coroutineScope { // launch a new coroutine for async font lookup //sampleStart val font = async { fonts(source = "urlToFont") { family = "Roboto"; size = 14; weight = 400 } } //sampleEnd //... font.await() }
tip

Since font loading is async, wrap it in a timeout if you don't want to wait forever — launch the load as a Job, and cancel() it after a delay with Scheduler:

package rendering import io.nacular.doodle.drawing.Font 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 { var font: Font? //sampleStart val job = launch { font = fonts { family = "Roboto"; size = 14; weight = 400; } } scheduler.after(5 * seconds) { job.cancel() } //sampleEnd }
Module Required

Include the FontModule (Web, Desktop) in your application to use FontLoader.

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 }

Measuring text​

Since text is positioned explicitly, centering or aligning it means knowing its size first — that's what TextMetrics is for, and happily, it's available for injection by default, no module required.

Here's a View putting that to use, centering text based on its own measured size:

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

render can run more often than your View's size actually changes, so it's worth computing the text's position only when size changes, rather than on every render call.

Recap​

  • text draws at an explicit position; wrapped draws multi-line text that wraps to a given width.
  • AnnotatedString> lets you style parts of a run of text differently, and still draws through text or wrapped.
  • Fonts load asynchronously via FontLoader (needs FontModule) — wrap the load in a timeout if you don't want to wait forever.
  • TextMetrics> measures text before you draw it, which is how you'd center or align it precisely. It needs no module.

Next: head to Images to load and draw images.