Skip to main content

New in Doodle 0.10.0

Released: February 20, 2024

The latest version of Doodle brings lots of important updates, especially in terms of better platform support for both Browser and Desktop. Some of the key highlights include:

  • New ability to host embed arbitrary HTML elements as Views on Web
  • WASM JS support
  • Multiple windows in Desktop apps
  • OS menu bars in Desktop
  • More native context menus in Desktop apps

Browser

Hosting arbitrary HTML elements

You can now embed any HTML element into your app as a View. This means Doodle apps can now host React and other web components and interop with a much larger part of the Web ecosystem out of the box!

Mon
Tue
Wed
Thu
Fri
Sat
Sun
package io.nacular.doodle.docs.apps import io.nacular.doodle.HtmlElementViewFactory import io.nacular.doodle.animation.Animator import io.nacular.doodle.application.Application import io.nacular.doodle.controls.text.Label import io.nacular.doodle.core.Display import io.nacular.doodle.core.then import io.nacular.doodle.docs.utils.DateRangeSelectionModel import io.nacular.doodle.docs.utils.HorizontalCalendar import io.nacular.doodle.docs.utils.ShadowCard import io.nacular.doodle.drawing.Font import io.nacular.doodle.geometry.PathMetrics import io.nacular.doodle.layout.constraints.constrain import io.nacular.doodle.theme.Theme import io.nacular.doodle.theme.ThemeManager import kotlinx.datetime.DatePeriod import kotlinx.datetime.LocalDate import kotlinx.datetime.plus import org.w3c.dom.HTMLElement class ReactCalendarApp( display : Display, font : Font, today : LocalDate, animate : Animator, pathMetrics : PathMetrics, themeManager : ThemeManager, theme : Theme, htmlElementView: HtmlElementViewFactory, reactCalendar : HTMLElement, appHeight : (Double) -> Unit ): Application { private val doodleCalendar = HorizontalCalendar( today, animate, pathMetrics, startDate = today, endDate = today + DatePeriod(years = 10), DateRangeSelectionModel() ).apply { this.font = font } init { themeManager.selected = theme //sampleStart display += Label("Doodle").apply { this.font = font } display += ShadowCard(doodleCalendar) display += Label("React").apply { this.font = font } display += htmlElementView(element = reactCalendar) //sampleEnd val spacing = 20 display.layout = constrain( display.children[0], display.children[1], display.children[2], display.children[3] ) { doodleLabel, doodle, reactLabel, react -> doodle.top eq doodleLabel.bottom + spacing doodle.height eq 280 react.height eq doodle.height doodleLabel.top eq spacing doodleLabel.centerX eq doodle.centerX doodleLabel.width.preserve doodleLabel.height.preserve reactLabel.centerX eq react.centerX reactLabel.width.preserve reactLabel.height.preserve when { parent.width.readOnly > 800 -> { doodle.width eq (parent.width - 3 * spacing) / 2 doodle.right eq parent.centerX - spacing / 2 react.top eq doodle.top react.width eq doodle.width react.left eq doodle.right + spacing reactLabel.top eq doodleLabel.top } else -> { doodle.left eq spacing doodle.right eq parent.right - spacing react.top eq reactLabel.bottom + spacing react.left eq spacing react.right eq parent.right - spacing reactLabel.top eq doodle.bottom + spacing } } }.then { container -> // signal to outer docs about height of the app appHeight(container.children.maxOf { it.bounds.bottom } + spacing) } } override fun shutdown() { // no-op } }
info

This app embeds a react-calendar.

WASM JS

Doodle now supports the WasmJS build target. This means apps can also target WebAssembly for the Browser. The APIs/features for this new target are identical as those for the js target; which means code can be shared between apps targeting both. The only difference is that the application launchers need to be called from separate source sets (i.e. jsMain vs wasmJsMain).

Desktop

Multi-window apps

Apps for Desktop can now create/manage multiple windows using the new WindowGroup interface. Simply inject it into your app to get started. The API provides access to an app's main window as well as methods for creating new windows. Single window apps continue to work as they did before. That is, an app that injects the Display will receive the main window display and can manipulate it as before. But apps that want to manage their window(s) will need to inject this new type.

package display import io.nacular.doodle.application.Application import io.nacular.doodle.core.WindowGroup import io.nacular.doodle.core.view import io.nacular.doodle.geometry.Size import io.nacular.doodle.layout.constraints.constrain import io.nacular.doodle.layout.constraints.fill //sampleStart class MyCoolApp(windows: WindowGroup): Application { init { // main window's display, same as if Display were injected windows.main.apply { title = "Main Window" // manipulate main window's display display += view {} } // create a new window windows { title = "A New Window!" size = Size(500) enabled = false resizable = false triesToAlwaysBeOnTop = true // manipulate the new window's display display += view {} display.layout = constrain(display.first(), fill) closed += { // handle window close } } } override fun shutdown() {} } //sampleEnd
tip

There's no need to inject Display if you already inject WindowGroup. That's because the injected Display is equivalent to windowGroup.main.display

Native window menus

Apps can now set up native menus for their windows. This looks a lot like working with the existing menu APIs, but it results in changes to the OS window decoration. These menus are just as interactive as the in-app ones as well, meaning they trigger events when the user interacts with them.

package display import io.nacular.doodle.controls.popupmenu.MenuBehavior.ItemInfo import io.nacular.doodle.core.Icon import io.nacular.doodle.core.Window fun example(window: Window, icon1: Icon<ItemInfo>, icon2: Icon<ItemInfo>) { //sampleStart window.menuBar { menu("Menu 1") { action("Do action 2", icon1) { /*..*/ } menu("Sub menu") { action("Do action sub", icon = icon2) { /*..*/ } separator() prompt("Some Prompt sub") { /*..*/ } } separator() prompt("Some Prompt") { /*..*/ } } menu("Menu 2") { // ... } } //sampleEnd }

Native context menus

Apps can now set up native context/popup menus for their windows. The API is very similar to native menus.

package display import io.nacular.doodle.controls.popupmenu.MenuBehavior.ItemInfo import io.nacular.doodle.core.Icon import io.nacular.doodle.core.Window import io.nacular.doodle.geometry.Point fun contextMenu(window: Window, icon1: Icon<ItemInfo>, icon2: Icon<ItemInfo>) { //sampleStart window.popupMenu(at = Point()) { action("Do action 2", icon1) { /*..*/ } menu("Sub menu") { action("Do action sub", icon = icon2) { /*..*/ } separator() prompt("Some Prompt sub") { /*..*/ } } separator() prompt("Some Prompt") { /*..*/ } } //sampleEnd }

All Platforms

Key event filters and bubbling

Key events now "sink" and "bubble" like pointer events. This means ancestor Views can intercept (and veto) them before they are delivered to their target (the focused View). They also bubble up to ancestors after being delivered to the target if they are not consumed. The notifications for the first phase happen via a new keyFilter property, while the bubbling phase is notified via the existing keyChanged property.

This change makes it much easier to create Views like the following; which intercepts the ENTER key to press the submit button.

package io.nacular.doodle.docs.apps import io.nacular.doodle.application.Application import io.nacular.doodle.controls.buttons.PushButton import io.nacular.doodle.controls.form.Always import io.nacular.doodle.controls.form.Form import io.nacular.doodle.controls.form.labeled import io.nacular.doodle.controls.form.textField import io.nacular.doodle.controls.form.verticalLayout import io.nacular.doodle.controls.text.TextField import io.nacular.doodle.core.Display import io.nacular.doodle.drawing.Font import io.nacular.doodle.event.KeyCode.Companion.Enter import io.nacular.doodle.event.KeyListener import io.nacular.doodle.geometry.Point import io.nacular.doodle.geometry.Size import io.nacular.doodle.layout.constraints.constrain import io.nacular.doodle.theme.Theme import io.nacular.doodle.theme.ThemeManager import io.nacular.doodle.utils.Resizer class EnterKeyInterceptApp( display : Display, font : Font, themeManager: ThemeManager, theme : Theme ): Application { private lateinit var name : TextField private lateinit var password: TextField private val submit = PushButton("Submit").apply { this.font = font this.size = Size(100, 32) this.enabled = false this.fired += { name.text = "" password.text = "" } } private val form = Form { this ( + labeled("Name", showRequired = Always()) { textField(Regex(".{3,}")) { name = textField } }, + labeled("Password", showRequired = Always()) { textField(Regex(".{3,}")) { password = textField } }, onInvalid = { submit.enabled = false }, ) { _,_ -> submit.enabled = true } }.apply { this.font = font this.size = Size(300, 100) this.layout = verticalLayout(this, spacing = 12.0, itemHeight = 32.0) this.focusable = false Resizer(this).apply { movable = false } } init { themeManager.selected = theme //sampleStart form.keyFilter += KeyListener.pressed { if (it.code == Enter && submit.enabled) { it.consume() submit.click() } } //sampleEnd display += listOf(form, submit) display.layout = constrain(form, submit) { form_, submit_ -> val spacing = 10 form_.center eq parent.center - Point(y = (spacing + submit_.height.readOnly) / 2) submit_.top eq form_.bottom + spacing submit_.centerX eq form_.centerX } } override fun shutdown() {} }