Web
Coming from Desktop, let's close things out with what's unique to running in the browser — and it's a satisfying place to land, since two things get promised earlier in this guide and finally paid off here.
Host apps in HTML elements
Back on Displays, this was promised but not shown: launching a top-level Web app inside a specific HTML element, rather than letting it take over the whole page. Here it is — pass the element to application, and everything else about your app is completely unaffected. (Every example throughout this documentation is, in fact, hosted exactly this way.)
package htmlelementapp
import usefulapp.HelloApp
import io.nacular.doodle.application.application
import org.w3c.dom.HTMLElement
//sampleStart
fun main(element: HTMLElement) {
// launch app within element
application(element) {
HelloApp()
}
}
//sampleEnd
Cleanup happens automatically, in more ways than one: closing the page tears down every app within it, and so does removing the element hosting a given app — or you can call shutdown on it explicitly, same as always.
Nested apps
The other promise, from all the way back on Applications: a Doodle Web app can run inside another Doodle Web app. Place the nested app in a View that the host app manages, and it behaves exactly like a top-level app in every way that matters — the one difference is its lifecycle, which is now tied to that host View instead of the page.
Build one with an ApplicationViewFactory, available through the AppViewModule:
- Inner App
- Outer App
- Outer App Launcher
package io.nacular.doodle.docs.apps.nesting
import io.nacular.doodle.application.Application
import io.nacular.doodle.controls.ColorPicker
import io.nacular.doodle.core.Display
import io.nacular.doodle.docs.utils.doodleColor
import io.nacular.doodle.drawing.opacity
import io.nacular.doodle.layout.Insets
import io.nacular.doodle.layout.constraints.constrain
import io.nacular.doodle.layout.constraints.fill
//sampleStart
class InnerApp(display: Display): Application {
init {
// Shows a color picker
display += ColorPicker(doodleColor opacity 0.75f).apply {
changed += { _,_,new -> println(new) }
}
// The picker grows with the display, but is inset a little
display.layout = constrain(display.first(), fill(insets = Insets(2.0)))
}
override fun shutdown() {}
}
//sampleEndpackage io.nacular.doodle.docs.apps.nesting
import io.nacular.doodle.application.Application
import io.nacular.doodle.application.ApplicationViewFactory
import io.nacular.doodle.application.Modules.Companion.PointerModule
import io.nacular.doodle.core.Display
import io.nacular.doodle.core.center
import io.nacular.doodle.core.height
import io.nacular.doodle.core.width
import io.nacular.doodle.drawing.Color.Companion.White
import io.nacular.doodle.drawing.paint
import io.nacular.doodle.geometry.Rectangle
import io.nacular.doodle.geometry.centered
import io.nacular.doodle.utils.Resizer
import org.kodein.di.instance
import kotlin.math.min
//sampleStart
class OuterApp(display: Display, appView: ApplicationViewFactory): Application {
init {
// NOTE: Any module needed by InnerApp MUST be provided to the factory.
// PointerModule used here to enable interaction with the color picker.
display += appView(modules = listOf(PointerModule)) {
InnerApp(display = instance()) // Init inner app
}.apply {
suggestBounds(Rectangle(
min(400.0, display.width - 20),
min(300.0, display.height - 20)
).centered(at = display.center))
Resizer(this, movable = false)
}
display.fill(White.paint)
}
override fun shutdown() {}
}
//sampleEndpackage outerapp
import io.nacular.doodle.application.ApplicationViewFactory.Companion.AppViewModule
import io.nacular.doodle.application.Modules.Companion.PointerModule
import io.nacular.doodle.application.application
import io.nacular.doodle.docs.apps.nesting.OuterApp
import org.kodein.di.instance
fun main() {
//sampleStart
// NOTE: Modules used by the outer app are not available to the inner one.
// The PointerModule is used here to allow resizing of the View that holds the inner app.
application(modules = listOf(AppViewModule, PointerModule)) {
OuterApp(display = instance(), appView = instance())
}
//sampleEnd
}
Host arbitrary HTML elements
This is the one that really opens things up: any HTML element can be embedded into your app as a View. That means a Doodle app can host React components — or anything else from the Web ecosystem — right alongside its own Views, not walled off from them.
The example below embeds a real react-calendar component next to a Doodle-native calendar built with the same constraints you've been using throughout this guide:
- 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.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.Strength.Companion.Required
import io.nacular.doodle.layout.constraints.constrain
import io.nacular.doodle.layout.constraints.ideal
import io.nacular.doodle.layout.constraints.minus
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 = today,
animate = animate,
pathMetrics = pathMetrics,
startDate = today,
endDate = today + DatePeriod(years = 10),
selectionModel = DateRangeSelectionModel()
).apply {
this.font = font
}
init {
themeManager.selected = theme
//sampleStart
display += Label("Doodle").apply { this.font = font }
display += Label("React" ).apply { this.font = font }
display += ShadowCard(doodleCalendar)
display += htmlElementView(element = reactCalendar)
//sampleEnd
val spacing = 20
display.children.last().boundsChanged += { _,_,new ->
// signal to outer docs about height of the app
appHeight(display.children.maxOf { it.bounds.bottom } + spacing)
}
display.layout = constrain(
display.children[0],
display.children[1],
display.children[2],
display.children[3]
) { doodleLabel, reactLabel, doodle, 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.size prefers ideal
reactLabel.centerX eq react.centerX
reactLabel.size prefers ideal
when {
parent.width.value > 800 -> {
doodle.width eq (parent.width - 3 * spacing) / 2 strength Required - 1
doodle.right eq parent.centerX - spacing / 2 strength Required - 1
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 strength Required - 1
react.top eq reactLabel.bottom + spacing
react.left eq spacing
react.right eq doodle.right
reactLabel.top eq doodle.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
}
Recap
- Launch a Web app inside a specific element with
applicationinstead of taking over the whole page — this is how every live example in this documentation actually runs. - A nested app behaves just like a top-level one, except its lifecycle is tied to the
Viewhosting it rather than the page. - Any HTML element — including a whole React component — can become a
Viewand sit directly alongside your Doodle content.
That's the whole guide. From Hello Doodle through here, you've now seen every piece: building and launching an app, the Display and View hierarchy, drawing with Canvas, sizing and positioning Views, responding to input, transforms and animation, drag-and-drop, popups, accessibility, theming and the full component library, and what's unique to each platform.
From here, the best next steps are outside this guided tour: Tutorials has complete, real-world apps to read through or fork, and the API Docs are there for whenever you need the exact signature of something rather than an explanation of it. Good luck building.