Skip to main content

Popups and modals

May apps need to show content above all other content periodically in the form of a popup, or overlay. You can do this manually by adding a top-level View to the Display, but this has limitations. This approach would subject the View to the Display's Layout, so controlling its position would be difficult. Moreover, new Views added to the Display could easily be placed above that View.

Doodle provides a solution to address this use case via the PopupManager. You can use this component to manage a set of Views that overcome the limitations mentioned above. Moreover, popups shown using the manager can be positioned with flexible and powerful constraints to ensure they remain visible, or change size based on the items around them.

Popups

Any View can be shown as a popup. This will add the it to the Display above all existing Views, including other popups that are visible. This is done by invoking show on the PopupManager.

Module Required

You must include the PopupModule in your application in order 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

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

The following shows a simple popup that is shown whenever you click on the blue rectangle. Clicking the popup hides it again.

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.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 { size = Size(400, 200) position = display.center - Point(width / 2, height / 2) // 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 do not prevent user interactions with the rest of the app.

There are many use cases where a popup's position needs to be tied to another View when it is shown. This is the case for drop down menus, hover callouts, tool tips, etc.. This is easy to achieve as well using the PopupManager. You simply use the variant of show that takes a relativeTo value. The manager will ensure the popup is positioned relative to the given View based on the constraints specified.

This app shows a popup that is positioned relative to the blue rectangle. Move the rectangle around while the popup is visible to see how it works.

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.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 { size = Size(300, 200) position = display.center - Point(width / 2, height / 2) // show popup when blue view clicked pointerChanged += clicked { popups.show(popup, relativeTo = this) { popup, blueView -> // size / position popup (popup.top eq blueView.y ) .. Strong (popup.left eq blueView.right + 10) .. Strong (popup.bottom lessEq parent.bottom - 5) .. 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

Modals are popups that can disable further interaction with the app (except for subsequent modals of course). They are useful when some user input is needed before proceeding. You can create a modal using any View (just like a popup) using the ModalManager. In fact, the ModalManager uses the PopupManager internally to manage modals.

Module Required

You must include the ModalModule in your application in order 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 }

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

Modals are asynchronous and return a value <T>. This means you have to call them from a CoroutineContext and you cannot get their result until they complete. This makes them ideal for user input.

You launch a modal by invoking the ModalManager with a lambda that returns a Modal<T> or RelativeModal<T>. The type used in the return determines the value the modal will complete with. Completion is handled by calling the completed method provided 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 }

This app shows a simple modal that drops down from the top and requires the user to dismissing it. This modal returns Unit as it's result when the user clicks 'Ok'.

This shows how you might present a choice using a modal. Here we ask the user to select between two Colors. This modal is centered (the default), it uses a different background treatment, and has a different animation when you click outside it.

Modals can also be positioned in relation to a View just like popups. Simple return a RelativeModal instead when creating 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 }