Skip to main content

Popups & Modals

Coming off Drag & Drop, let's look at another kind of "outside the normal hierarchy" content: things that need to float above everything else.

Many apps need to show content above everything else from time to time — a popup, an overlay, a dropdown. You could do this by hand, adding a top-level View straight to the Display — but you'd quickly run into two problems. That View would be subject to the Display's own Layout, making precise positioning a hassle. And any other top-level View added later could just as easily end up stacked above it.

Doodle solves both problems with PopupManager, which manages a set of Views that sidestep those limitations entirely; and positions them with the same flexible constraints you already know, so they can stay visible or resize based on whatever's around them.

Popups​

Any View can become a popup. Call show on the PopupManager, and it lands on the Display above every existing View — other visible popups included.

Module Required

Include the PopupModule (Web, Desktop) in your application to use these features.

package popups import io.nacular.doodle.application.Application import io.nacular.doodle.application.Modules.Companion.PopupModule import io.nacular.doodle.application.application import io.nacular.doodle.controls.PopupManager import io.nacular.doodle.core.Display import org.kodein.di.instance class PopupApp(display: Display, popupManager: PopupManager): Application { init { // .. } override fun shutdown() {} } //sampleStart fun main() { application(modules = listOf(PopupModule)) { PopupApp(display = instance(), popupManager = instance()) } } //sampleEnd

Here's a popup that appears when you click a blue rectangle, and disappears when you click it back:

package io.nacular.doodle.docs.apps import io.nacular.doodle.application.Application import io.nacular.doodle.controls.PopupManager import io.nacular.doodle.core.Display import io.nacular.doodle.core.center import io.nacular.doodle.docs.utils.DEFAULT_FONT_FAMILIES import io.nacular.doodle.docs.utils.DEFAULT_FONT_SIZE import io.nacular.doodle.docs.utils.blueClick import io.nacular.doodle.docs.utils.clickView import io.nacular.doodle.drawing.Color.Companion.White import io.nacular.doodle.drawing.FontLoader import io.nacular.doodle.drawing.TextMetrics import io.nacular.doodle.drawing.paint import io.nacular.doodle.event.PointerListener.Companion.clicked import io.nacular.doodle.geometry.Point import io.nacular.doodle.geometry.Rectangle import io.nacular.doodle.geometry.Size import io.nacular.doodle.image.ImageLoader import io.nacular.doodle.utils.Resizer import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch class SimplePopup( display : Display, popups : PopupManager, fonts : FontLoader, images : ImageLoader, textMetrics : TextMetrics, uiDispatcher: CoroutineDispatcher, ): Application { init { val appScope = CoroutineScope(SupervisorJob() + uiDispatcher) appScope.launch { val font = fonts { families = DEFAULT_FONT_FAMILIES size = DEFAULT_FONT_SIZE * 2 }!! val image = images.load("/doodle/images/touch.svg")!! val popup = clickView(textMetrics).apply { this.font = font pointerChanged += clicked { popups.hide(this) } } //sampleStart // Hide popup when it is clicked popup.pointerChanged += clicked { popups.hide(popup) } display += blueClick(image).apply { suggestBounds(Rectangle(display.center - Point(400 / 2, 200 / 2), Size(400, 200))) // show popup when blue view clicked pointerChanged += clicked { popups.show(popup) { // size / position popup it.height eq parent.height / 2 it.width eq it.height * 1.5 it.center eq parent.center } } Resizer(this) } //sampleEnd display.fill(White.paint) } } override fun shutdown() {} }
info

Unlike Modals, regular popups don't block interaction with the rest of the app.

Relative positioning​

Plenty of popups need to stay tethered to another View — dropdown menus, hover callouts, tooltips, that sort of thing. PopupManager makes this just as easy: reach for the variant of show that takes a relativeTo value, and the manager positions the popup relative to that View, according to whatever constraints you give it.

Here's a popup that stays anchored to a blue rectangle — drag the rectangle around while the popup is showing, and watch the popup follow:

package io.nacular.doodle.docs.apps import io.nacular.doodle.application.Application import io.nacular.doodle.controls.PopupManager import io.nacular.doodle.core.Display import io.nacular.doodle.core.center import io.nacular.doodle.docs.utils.DEFAULT_FONT_FAMILIES import io.nacular.doodle.docs.utils.DEFAULT_FONT_SIZE import io.nacular.doodle.docs.utils.blueClick import io.nacular.doodle.docs.utils.clickView import io.nacular.doodle.drawing.Color.Companion.White import io.nacular.doodle.drawing.FontLoader import io.nacular.doodle.drawing.TextMetrics import io.nacular.doodle.drawing.paint import io.nacular.doodle.event.PointerListener.Companion.clicked import io.nacular.doodle.geometry.Point import io.nacular.doodle.geometry.Rectangle import io.nacular.doodle.geometry.Size import io.nacular.doodle.image.ImageLoader import io.nacular.doodle.layout.constraints.Strength.Companion.Strong import io.nacular.doodle.utils.Resizer import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch class RelativePopup( display : Display, popups : PopupManager, fonts : FontLoader, images : ImageLoader, textMetrics : TextMetrics, uiDispatcher: CoroutineDispatcher, ): Application { init { val appScope = CoroutineScope(SupervisorJob() + uiDispatcher) appScope.launch { val font = fonts { families = DEFAULT_FONT_FAMILIES size = DEFAULT_FONT_SIZE * 2 }!! val image = images.load("/doodle/images/touch.svg")!! val popup = clickView(textMetrics).apply { this.font = font pointerChanged += clicked { popups.hide(this) } } //sampleStart // Hide popup when it is clicked popup.pointerChanged += clicked { popups.hide(popup) } display += blueClick(image).apply { suggestBounds(Rectangle(display.center - Point(300 / 2, 200 / 2), Size(300, 200))) // show popup when blue view clicked pointerChanged += clicked { popups.show(popup, relativeTo = this) { popup, blueView -> // size / position popup popup.top eq blueView.y strength Strong popup.left eq blueView.right + 10 strength Strong popup.bottom lessEq parent.bottom - 5 strength Strong popup.top greaterEq 5 popup.left greaterEq 5 popup.right lessEq parent.right - 5 popup.height eq parent.height / 2 popup.width eq popup.height * 1.5 } } Resizer(this) } //sampleEnd display.fill(White.paint) } } override fun shutdown() {} }

Modals​

A modal is a popup with one extra super-power: it can block further interaction with the rest of your app (aside from any modal that comes after it). That makes modals a natural fit whenever you genuinely need an answer from the user before anything else can proceed. You build one from any View, the same way you'd build a popup, using ModalManager — which, fittingly, uses PopupManager under the hood.

Module Required

Include the ModalModule (Web, Desktop) in your application to use these features.

package popups import io.nacular.doodle.application.Application import io.nacular.doodle.application.Modules.Companion.ModalModule import io.nacular.doodle.application.application import io.nacular.doodle.controls.modal.ModalManager import io.nacular.doodle.core.Display import org.kodein.di.instance class ModalApp(display: Display, modalManager: ModalManager): Application { init { // .. } override fun shutdown() {} } fun example() { //sampleStart fun main() { application(modules = listOf(ModalModule)) { ModalApp(display = instance(), modalManager = instance()) } } //sampleEnd }

Here's the detail that sets modals apart: they're asynchronous, and they return a value of type <T>. That means you call them from a CoroutineContext, and you simply can't get a result until the modal completes — which turns out to be exactly the shape you want for collecting user input.

Launch one by invoking ModalManager with a lambda returning a Modal<T> or a RelativeModal<T>. Whichever type you return determines the value the modal eventually completes with — and completion itself happens by calling completed from within the creation context:

package popups import io.nacular.doodle.controls.modal.ModalManager import io.nacular.doodle.controls.modal.ModalManager.Modal import io.nacular.doodle.core.view suspend fun <T> modalCreation(modal: ModalManager, result: T) { //sampleStart // launch modal and await result (suspending) val value: T = modal { Modal( // View used as modal view { // ... // call completed with user input when done completed(result) } ) { // optionally provide a layout block // or the view will default to being // displayed in the center } } //sampleEnd }

Two live demos are worth playing with below. The first is a simple modal that drops down from the top and waits for the user to dismiss it, completing with Unit once they click "Ok"

The second is asks the user to choose between two Color`s, with its own background treatment and dismiss animation.

Relative positioning for modals​

Modals can anchor to another View, exactly the way popups do — just return a RelativeModal instead when you create one

package popups import io.nacular.doodle.controls.modal.ModalManager import io.nacular.doodle.controls.modal.ModalManager.RelativeModal import io.nacular.doodle.core.view suspend fun <T> relativeModal(modal: ModalManager, result: T) { //sampleStart val someView = view {} // launch modal and await result (suspending) val value: T = modal { RelativeModal( // View used as modal view { // ... // call completed when the modal is done completed(result) }, relativeTo = someView ) { modal, someViewBounds -> // position relative to parent and someView } } //sampleEnd }

Recap​

  • PopupManager> floats any View above the rest of your app, sidestepping the Display's own Layout> and stacking-order limitations.
  • Popups don't block interaction; modals do — and modals are suspend, returning a value once the user responds.
  • Position either one relative to another View with the relativeTo variant of show, or by returning a RelativeModal instead of a Modal.

Next: on to Accessibility, the last stop in Advanced Topics.