Skip to main content

Rendering

Doodle automatically manages rendering of Views, and this covers almost all use-cases. Each View draws its content to a Canvas provided during calls to the render method. This either presents the View's contents on the screen for the first time or updates them on subsequent calls.

Doodle calls render whenever a View needs a visual update, which includes changes to its size or it becoming visible. This means you do not need to call render yourself. But there might be cases when you want to request that a View re-renders. You can do this by calling rerender.

This is an example of a simple View that draws a rectangle filled with Blue ColorPaint that covers its bounds.

import io.nacular.doodle.core.View import io.nacular.doodle.drawing.Canvas import io.nacular.doodle.drawing.Color.Companion.Blue import io.nacular.doodle.drawing.paint //sampleStart class RectView: View() { override fun render(canvas: Canvas) { canvas.rect(bounds.atOrigin, Blue.paint) } } //sampleEnd
tip

render is automatically called on size changes and visible changing to true

The Canvas

All drawing is done via the Canvas API, which offers a rich set of operations for geometric shapes, paths, images, and text. It also supports different Paint types (i.e. ColorPaint, LinearGradientPaint, PatternPaint, etc.) for filling regions.

The Canvas provided to a View's render method has a coordinate system that anchors 0,0 on the Canvas to the View's origin. The Canvas itself extends in all directions beyond the bounds of the View; but the contents drawn to it will be clipped to the view's bounds by default.

tip

Sub-classes can disable clipping by setting clipCanvasToBounds = false.

Most Views require geometric regions on their Canvas to be filled with some color, gradient, texture, etc.. Therefore, many Canvas APIs take a Paint parameter that determines how the inner regions of these shapes are filled. There are several built-in Paint types to choose from.

Transformations

The Canvas can also be transformed using any AffineTransform. These are linear transformation which encapsulate translation, scaling, and rotation in matrices. You can combine these transforms into more complex ones directly, or apply them to a Canvas in a nested fashion to get similar results

This View flips the Canvas horizontally around its mid-point and draws some text.

package rendering import io.nacular.doodle.core.View import io.nacular.doodle.drawing.Canvas import io.nacular.doodle.drawing.Color.Companion.Black import io.nacular.doodle.drawing.text //sampleStart class MyView: View() { override fun render(canvas: Canvas) { canvas.flipHorizontally(around = width / 2) { text("hello", color = Black) } } } //sampleEnd

3D rendering

Canvas offers basic 3D capabilities through the use of AffineTransforms and Cameras. This enables Views to display content in a shared 3D space.

The following examples has a View that draws a 3D cube. The cube looks like a 3D object even though it is flat on the View's surface. The controls (which are overlaid on the View) let you change the starting transform applied to the cube's back face (which changes the cube's overall transform), the folding angle of each face, and the camera used to create the perspective.

Filling regions with Paints

Colors

Doodle supports RGBA colors (via the Color class), for rendering content to the Canvas via paints like ColorPaint. Colors can be created explicitly using hex notation, or you can use any of the built-in values (i.e. Red). Colors can also be derived from others with one of the many utility functions like lighter, darker, or by changing opacity.

package rendering import io.nacular.doodle.drawing.Color import io.nacular.doodle.drawing.darker import io.nacular.doodle.drawing.opacity import io.nacular.doodle.drawing.paint fun colors() { //sampleStart Color(0xff0000u ) // create red Color(0xffffffu, opacity = 0.5f) // white with 0.5 opacity Color.Red // build-in red Color.Blue opacity 0.5f // blue with 0.5 opacity Color.Blue.inverted // inverse of blue Color.Lightgray.darker(0.5f) // darker gray Color.Blue.paint // paint from a color //sampleEnd }
info

Sometimes it is more effective to work within other color spaces. Doodle has built-in utilities for HSL and HSV colors, though these colors cannot be used directly for rendering. That said, there are many utilities for easily transforming between them and RGBA.

The ColorPicker controls uses HSV internally for example.

ColorPaint is the simplest and most common way to fill a region. You can create one using the constructor, or via the paint extension on any Color.

package rendering import io.nacular.doodle.core.view import io.nacular.doodle.drawing.Color import io.nacular.doodle.drawing.paint fun colorPaint(color: Color) { //sampleStart view { render = { rect(bounds.atOrigin, color.paint) } } //sampleEnd }

Gradients

You can fill regions with LinearGradientPaints, RadialGradientPaints and SweepGradientPaints paints as well. These take a series of Colors and stop locations that are turned into a smoothly transitioning gradient across a shape.

package rendering import io.nacular.doodle.core.view import io.nacular.doodle.drawing.Color import io.nacular.doodle.drawing.GradientPaint.Stop import io.nacular.doodle.drawing.LinearGradientPaint import io.nacular.doodle.geometry.Point /** * Example showing how to use [LinearGradientPaint]s. */ fun linearPaint(color1: Color, color2: Color, point1: Point, point2: Point) { //sampleStart view { render = { // Simple version with 2 colors rect(bounds.atOrigin, LinearGradientPaint( color1, color2, point1, point2 )) } } view { render = { // Also able to use a list of color stops rect(bounds.atOrigin, LinearGradientPaint( listOf( Stop(color1, 0f ), Stop(color1, 1f/3), // ... ), point1, point2 )) } } //sampleEnd }

Images

Images can be used to fill regions with a repeating pattern. This paint takes an Image along with a size and opacity that image should be rendered at. The result is a repeating pattern that tiles the shape being filled.

info

ImagePaint does not allow any cropping of the provided image. But you can use PatternPaint to do this.

Patterns

Sometimes you need to fill a region with a repeating pattern, like an image or some geometric shapes. This is easy to achieve with ImagePaint (when an image is all you need) or PatternPaint, when more sophisticated patterns are needed.

PatternPaint has a "render" body that provides a powerful and familiar way of creating repeating patterns. You create one by specifying a size and a paint lambda, which has access to the full Canvas APIs.

This app uses StripedPaint to show how a PatternPaint can be transformed using AffineTransform2D, like rotated around its center for example.

package rendering import io.nacular.doodle.core.view import io.nacular.doodle.drawing.AffineTransform2D import io.nacular.doodle.drawing.PatternPaint import io.nacular.doodle.geometry.Size fun patternPaint(size: Size, transform: AffineTransform2D) { //sampleStart view { render = { rect(bounds.atOrigin, PatternPaint(size, transform) { // render onto canvas // rect(..) }) } } //sampleEnd }

Frosted Glass

The FrostedGlassPaint lets you emulate the look of glass that blurs the contents below it. The paint takes a blurRadius and an optional Color.

package rendering import io.nacular.doodle.core.view import io.nacular.doodle.drawing.Color.Companion.White import io.nacular.doodle.drawing.FrostedGlassPaint import io.nacular.doodle.drawing.opacity /** * Example showing how to use [FrostedGlassPaint]s. */ fun frostedGlassPaint() { //sampleStart view { render = { rect(bounds.atOrigin, FrostedGlassPaint( color = White opacity 0.5f, blurRadius = 10.0 )) } } //sampleEnd }

Outlining shapes with Strokes

Regions and curves can be stroked to give them an outline of some sort. The Canvas APIs also take Stroke parameters that determine how to outline the shape being rendered.

A Stroke combines the behavior of a Paint (but for the outline only) and adds several properties that govern how the outline is created.

info

Strokes are centered along the edge of the shape the outline. This means adding a stroke will expand the area needed to render the shape by stroke.thickness / 2 in the x and y direction. Keep this in mind, and shrink shapes accordingly to ensure outlines are not clipped and they appear where you want them to.

package rendering import io.nacular.doodle.core.view import io.nacular.doodle.drawing.Paint import io.nacular.doodle.drawing.Stroke import io.nacular.doodle.drawing.Stroke.LineCap.Round import io.nacular.doodle.drawing.Stroke.LineJoint.Miter fun stroke(paint: Paint) { //sampleStart view { render = { rect(bounds.atOrigin, stroke = Stroke( fill = paint, dashes = doubleArrayOf(10.0, 20.0), thickness = 2.5, lineCap = Round, lineJoint = Miter )) } } //sampleEnd }

Basic shapes

The Canvas has APIs for drawing many basic shapes that foundation of most Views. Each shape can be filled with Paint and outlined with Strokes as well.

package rendering import io.nacular.doodle.core.view import io.nacular.doodle.drawing.Paint import io.nacular.doodle.drawing.Stroke import io.nacular.doodle.geometry.Rectangle fun rect(rectangle: Rectangle, paint: Paint, stroke: Stroke) { //sampleStart view { render = { rect(rectangle, fill = paint, stroke = stroke) } } //sampleEnd }
tip

A View's bounds is relative to its parent, so you need to do bring it into the View's coordinate space before drawing it to the Canvas. This is easy with bounds.atOrigin.

Paths

Paths a really powerful primitives that let you create arbitrarily complex shapes that can be filled and stroked like those above. This flexibility comes from the fact that paths are fairly opaque components that behave almost like images.

A major difference with Paths is that they do not track their intrinsic size (though this can be obtained using a PathMetrics). This is a tradeoff that allows them to be created in a very light weight way. Paths can be transformed when rendered to a Canvas to adjust their size, position, etc.

package rendering import io.nacular.doodle.core.view import io.nacular.doodle.drawing.Paint import io.nacular.doodle.drawing.Stroke import io.nacular.doodle.geometry.Circle import io.nacular.doodle.geometry.Point import io.nacular.doodle.geometry.inscribed import io.nacular.doodle.geometry.path import io.nacular.doodle.geometry.rounded fun path(paint: Paint, stroke: Stroke) { // Path from SVG data val heart = path("M50,25 C35,0,-14,25,20,60 L50,90 L80,60 C114,20,65,0,50,25")!! // Star with rounded corners val star = Circle( radius = 100.0, center = Point(100, 100) ).inscribed(5)!!.rounded(radius = 10.0) //sampleStart view { render = { // transform the Canvas to change path bounds, etc. path(star, fill = paint, stroke = stroke) } } //sampleEnd }
Module Required

You must include the PathModule (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.PathModule import io.nacular.doodle.application.application import io.nacular.doodle.core.Display import io.nacular.doodle.geometry.PathMetrics import org.kodein.di.instance class PathApp(display: Display, pathMetrics: PathMetrics): Application { override fun shutdown() {} } fun pathModule() { //sampleStart application(modules = listOf(PathModule)) { PathApp(display = instance(), pathMetrics = instance()) } //sampleEnd }

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

tip

There are may ways to create Paths.

package rendering import io.nacular.doodle.geometry.Circle import io.nacular.doodle.geometry.Path import io.nacular.doodle.geometry.Point import io.nacular.doodle.geometry.Rectangle import io.nacular.doodle.geometry.map import io.nacular.doodle.geometry.path import io.nacular.doodle.geometry.toPath fun pathCreation(start: Point, end: Point, width: Double, height: Double) { //sampleStart // Just convert most geometric shapes into them: Circle().toPath() Rectangle().toPath() Rectangle().map { it * 2 }.toPath() // Use the path builder function: path(start) .cubicTo( Point(width / 2, height / 2), Point(width / 3, 0), Point(width / 2, -10) ) .cubicTo( end, Point(width / 2, height + 10), Point(width * 2/3, height) ).finish() // or close // Or use a raw String: val path: Path? = path("...") // returns nullable since given string might be invalid val imSure = path("...")!! //sampleEnd }

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 (and/or Stroke used for outlining). 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.

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.
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.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.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." 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 ) } //sampleEnd }.apply { suggestSize(Size(400, 200)) Resizer(this, 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() {} }

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 (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.

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 }

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 }

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.

Images

Images are treated like primitive elements of the rendering pipeline. They are rendered directly to a Canvas like other shapes and text. This means transformations and other rendering capabilities apply to images as well.

You are able to define the rectangular region within an image that will be put onto the Canvas, as well as where on the Canvas that region will be placed. These two values allow you to zoom and scale images as you draw them.

package io.nacular.doodle.docs.apps import io.nacular.doodle.application.Application import io.nacular.doodle.application.Modules.Companion.ImageModule import io.nacular.doodle.application.Modules.Companion.PointerModule import io.nacular.doodle.application.application import io.nacular.doodle.core.Display import io.nacular.doodle.core.center import io.nacular.doodle.core.height import io.nacular.doodle.core.view import io.nacular.doodle.core.width import io.nacular.doodle.docs.utils.controlBackgroundColor import io.nacular.doodle.drawing.paint import io.nacular.doodle.geometry.Rectangle import io.nacular.doodle.geometry.Size import io.nacular.doodle.geometry.centered import io.nacular.doodle.image.ImageLoader import io.nacular.doodle.utils.Resizer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import org.kodein.di.instance import org.w3c.dom.HTMLElement import kotlin.math.min class ImageApp( display : Display, imageLoader: ImageLoader, appScope : CoroutineScope ): Application { init { appScope.launch { //sampleStart val image = imageLoader.load("/doodle/images/photo.jpg") ?: return@launch display.children += view { render = { image(image, destination = bounds.atOrigin) } Resizer(this) } //sampleEnd display.fill(controlBackgroundColor.paint) when { display.size.empty -> display.sizeChanged += { _,_,_ -> setInitialBounds(display, image.size) } else -> setInitialBounds(display, image.size) } } } private fun setInitialBounds(display: Display, imageSize: Size) { with(display.first()) { if (size.empty && !display.size.empty) { var w = min(display.width / 2, display.height - 40) var h = w * imageSize.height / imageSize.width if (h > display.height - 40) { h = display.height - 40 w = h * imageSize.width / imageSize.height } display.first().suggestBounds(Rectangle(w, h).centered(display.center)) } } } override fun shutdown() {} companion object { private val appModules = listOf(PointerModule, ImageModule) operator fun invoke(root: HTMLElement) = application(root, modules = appModules) { ImageApp(instance(), instance(), CoroutineScope(SupervisorJob() + Dispatchers.Default)) } } }
tip

You can also control the corner radius, and opacity of the image being drawn.

You load Images into your app using the ImageLoader. This interface provides an async API for fetching images from different sources.

Module Required

You must include the ImageModule (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.ImageModule import io.nacular.doodle.application.application import io.nacular.doodle.core.Display import io.nacular.doodle.image.ImageLoader import org.kodein.di.instance class ImageLoaderApp(display: Display, images: ImageLoader): Application { override fun shutdown() {} } fun imageLoader() { //sampleStart application(modules = listOf(ImageModule)) { ImageLoaderApp(display = instance(), images = instance()) } //sampleEnd }

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

ImageLoader provides APIs for loading images from various sources, like urls, file-paths, and LocalFiles that are obtained during drag-drop or via a FileSelector.

The following examples shows how loading works. Notice that ImageLoader.load returns Image?, which is null when the image fails to load for some reason. Fetching is also async, so it must be done from a suspend method or a CoroutineScope.

package rendering import io.nacular.doodle.image.Image import io.nacular.doodle.image.ImageLoader import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch fun loadImage(imageLoader: ImageLoader, scope: CoroutineScope) { //sampleStart scope.launch { val image: Image? = imageLoader.load("some_image_path") // won't get here until load resolves image?.let { // ... } } //sampleEnd }
tip

See here for an example of how you might handle time-outs.

Efficient rendering

Doodle optimizes rendering to avoid re-applying operations when drawing the same content repeatedly. For example, the Timer app below renders the epoch time every millisecond. However, Doodle only updates the changing regions (i.e. the DOM for Web apps), which is the text in this case. This reduces the amount of thrash in the DOM for Web apps.

1743035805386 ms
package timerdsl 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.Color.Companion.Green import io.nacular.doodle.drawing.Color.Companion.Red import io.nacular.doodle.drawing.Stroke import io.nacular.doodle.drawing.rect import io.nacular.doodle.drawing.text import io.nacular.doodle.geometry.Size import io.nacular.doodle.scheduler.Scheduler import io.nacular.doodle.time.Clock import io.nacular.measured.units.Time.Companion.milliseconds import io.nacular.measured.units.times //sampleStart class Timer(display: Display, clock: Clock, scheduler: Scheduler): Application { init { display += view { suggestSize(Size(200)) scheduler.every(1 * milliseconds) { rerender() } render = { rect(bounds.at(x = 0.0, y = 20.0), color = Green) rect(bounds.atOrigin.inset(0.5), Stroke(Red)) text("${clock.epoch}", color = Black) } } } override fun shutdown() {} } //sampleEnd
info

Doodle uses Measured for time, angles etc.