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()
}
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
}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())
}
}
Recap
textdraws at an explicit position;wrappeddraws multi-line text that wraps to a given width.AnnotatedString> lets you style parts of a run of text differently, and still draws throughtextorwrapped.- Fonts load asynchronously via
FontLoader(needsFontModule) — 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.