New in Doodle 0.10.0
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!
- App
- Example Launcher
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
}
}
package elementview
import io.nacular.doodle.HtmlElementViewFactory
import io.nacular.doodle.application.Application
import io.nacular.doodle.application.HtmlElementViewModule
import io.nacular.doodle.application.Modules
import io.nacular.doodle.application.application
import io.nacular.doodle.core.Display
import org.kodein.di.instance
import org.w3c.dom.HTMLElement
private class MyApp(
display : Display,
viewFactory: HtmlElementViewFactory,
element : HTMLElement
): Application {
init {
display += viewFactory(element)
}
override fun shutdown() {}
}
fun main(element: HTMLElement) {
//sampleStart
application(modules = listOf(Modules.HtmlElementViewModule)) {
MyApp(display = instance(), viewFactory = instance(), element = element)
}
//sampleEnd
}
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
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() {}
}