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())
}
}
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.
You must include the FontModule
(Web, Desktop) 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
}