Skip to main content

User input

Pointer handling is easy with Doodle; simply include the PointerModule when launching your app, and the underlying framework uses it to produce key events.

Module Required

You must include the PointerModule (Web, Desktop) in your application in order to use these features.

package pointerinput import io.nacular.doodle.application.Application import io.nacular.doodle.application.Modules.Companion.PointerModule import io.nacular.doodle.application.application import io.nacular.doodle.core.Display import org.kodein.di.instance import rendering.MyApp //sampleStart fun main() { /** Include [PointerModule] when launching your app */ application(modules = listOf(PointerModule)) { MyApp(instance()) } } /** * Pointer events will fire for this app when launched with [PointerModule] */ class MyApp(display: Display): Application { override fun shutdown() {} } //sampleEnd

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

Pointer input

Hit detection

The framework relies on the View contains method to determine when the pointer is within a View's boundaries. This method gets a point in the view's parent reference frame (or the Display's for top-level Views)) and returns whether or not that point intersects the view. The default implementation just checks the point against bounds and accounts for the view's transform.

Support for transforms

Doodle also accounts for transformations applied to the View's ancestors when delivering pointer events. This means the View will receive the right notification whenever the pointer intersects its parent despite transformations. Hit detection logic in the View is then triggered as usual. The View still needs to take its own transformation into account though, since the given point used in hit detection is within the parent coordinate space. This is automatically handled if the View implements intersects or if toLocal is used when overriding contains.

tip

Override intersects instead of contains unless you want to factor in the view's transform into your hit detection logic.

Pointer listeners

Views are able to receive pointer events once the PointerModule (Web, Desktop) is loaded, they are visible and enabled. You can then attach a PointerListener to any View's pointerChanged property and get notified whenever a pointer does one of the following:

  • Enters the View
  • Pressed within the View
  • Released within the View
  • Clicked (Pressed then Released) within the View
  • Exits the View
package pointerinput import io.nacular.doodle.core.View import io.nacular.doodle.event.PointerEvent import io.nacular.doodle.event.PointerListener import io.nacular.doodle.event.PointerListener.Companion.on import io.nacular.doodle.event.PointerListener.Companion.pressed fun example(view: View) { //sampleStart // Listen to pressed/exit via interface override view.pointerChanged += object: PointerListener { override fun pressed(event: PointerEvent) { // .. } override fun exited(event: PointerEvent) { // .. } } // Listener to pressed via DSL view.pointerChanged += pressed { event -> /* .. */ } // Listen to pressed/exit via DSL view.pointerChanged += on( pressed = { event -> /* .. */ }, exited = { event -> /* .. */ }, ) //sampleEnd }
tip

PointerListener has no-op defaults for each event, so you only need to implement the ones you need.

info

Notice that pointerChanged--like other observable properties--supports many observers and enables you to add/remove an observer any time.

Pointer events

The event provided to PointerListeners carries information about the View it originated from (target), the View it is sent to (source), various attributes about the state of the pointers--like buttons pressed--and their locations relative to the target View.

Pointer events are consumable. This means any observer can call consume on an event and prevent subsequent listeners from receiving it.

package pointerinput import io.nacular.doodle.core.View import io.nacular.doodle.event.PointerListener.Companion.pressed fun consume(view: View) { //sampleStart view.pointerChanged += pressed { event -> // ... take action based on event event.consume() // indicate that no other listeners should be notified } //sampleEnd }
info

Calling consume during filter will prevent descendants (and the target) from receiving the event

Pointer motion events

Pointer motion events occur whenever a pointer moves within a View. They are treated separately from pointer events because of their high frequency. The PointerModule is also required to enable them. And hit detection follows the same rules as with pointer events.

Registration is different though. You use listen to pointerMotionChanged and implement PointerMotionListener.

Pointer motion listeners are notified whenever a pointer:

  • Moves within a View
  • Drags anywhere while pressed, if the press started in a View
package pointerinput import io.nacular.doodle.core.View import io.nacular.doodle.event.PointerEvent import io.nacular.doodle.event.PointerMotionListener import io.nacular.doodle.event.PointerMotionListener.Companion.moved import io.nacular.doodle.event.PointerMotionListener.Companion.on fun pointerMotion(view: View) { //sampleStart // Listen to moved/dragged via interface override view.pointerMotionChanged += object: PointerMotionListener { override fun moved(event: PointerEvent) { // .. } override fun dragged(event: PointerEvent) { // .. } } // Listener to moved via DSL view.pointerMotionChanged += moved { event -> /* .. */ } // Listen to moved/dragged via DSL view.pointerMotionChanged += on( moved = { event -> /* .. */ }, dragged = { event -> /* .. */ }, ) //sampleEnd }

Multi-touch

Pointer events support multiple, simultaneous inputs by default. This covers the multi-touch use-case on mobile and other similar scenarios. The PointerEvent class contains information about all active Interactions for the current event. This includes those directed at the event target. Apps are therefore able to incorporate this into their pointer handling.

package pointerinput import io.nacular.doodle.core.View import io.nacular.doodle.event.PointerMotionListener.Companion.moved fun multiTouch(view: View) { //sampleStart view.pointerMotionChanged += moved { event -> event.targetInteractions // the set of interactions with the target View event.changedInteractions // the interactions that changed (triggered) this event event.allInteractions // all active interactions for the app } //sampleEnd }

Doodle also does not limit simultaneous interactions to a single View. All active interactions will be sent to the appropriate Views and managed concurrently. This means it is possible to drag multiple items at the same time.

tip

Try moving both boxes at the same time if you are on a mobile device or have multiple pointers.

tip

Keyboard input

Key handling is simple with Doodle; simply include the KeyboardModule when launching your app, and the underlying framework uses it to produce key events.

Module Required

You must include the KeyboardModule (Web, Desktop) in your application in order to use these features.

package keyboard import io.nacular.doodle.application.Application import io.nacular.doodle.application.Modules.Companion.KeyboardModule import io.nacular.doodle.application.application import io.nacular.doodle.core.Display import org.kodein.di.instance import rendering.MyApp //sampleStart fun main() { /** Include [KeyboardModule] when launching your app */ application(modules = listOf(KeyboardModule)) { MyApp(instance()) } } /** * Key events will fire for this app when launched with [KeyboardModule] */ class MyApp(display: Display): Application { override fun shutdown() {} } //sampleEnd

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

A View must gain focus in order to begin receiving key events. This ensures that only a single View can receive key events at any time within the app.

Use the FocusManager to control focus. It is included in the KeyboardModule. Just inject it into your app to begin managing the focus.

package focus import io.nacular.doodle.application.Application import io.nacular.doodle.application.Modules.Companion.KeyboardModule import io.nacular.doodle.application.application import io.nacular.doodle.core.Display import io.nacular.doodle.core.view import io.nacular.doodle.focus.FocusManager import org.kodein.di.instance //sampleStart fun main() { application(modules = listOf(KeyboardModule)) { // FocusManager is available in the KeyboardModule MyApp(display = instance(), focusManager = instance()) } } class MyApp(display: Display, focusManager: FocusManager): Application { init { val view = view {} display += view // ... focusManager.requestFocus(view) // ... } override fun shutdown() {} } //sampleEnd
tip

Some controls (i.e. TextField) also manage their focus when styled in the native theme

Key Listeners

Views are able to receive key events once the KeyboardModule (Web, Desktop) is loaded and they have focus. You can then attach a KeyListener to any View and get notified whenever it has focus and a key is Pressed or Released.

You get these notifications by registering with a View's keyChanged property.

package keyboard import io.nacular.doodle.core.View import io.nacular.doodle.event.KeyEvent import io.nacular.doodle.event.KeyListener import io.nacular.doodle.event.KeyListener.Companion.on import io.nacular.doodle.event.KeyListener.Companion.pressed fun example(view: View) { //sampleStart // Listen to pressed/released via interface override view.keyChanged += object: KeyListener { override fun pressed(event: KeyEvent) { // .. } override fun released(event: KeyEvent) { // .. } } // Listener to pressed via DSL view.keyChanged += pressed { event -> /* .. */ } // Listen to pressed/released via DSL view.keyChanged += on( pressed = { event -> /* .. */ }, released = { event -> /* .. */ }, ) //sampleEnd }
tip

KeyListener has no-op defaults for the 2 events, so you only need to implement the ones you need.

info

Notice that keyChanged--like other observable properties--supports many observers and enables you to add/remove an observer any time.

Key events

The event provided to key listeners carries information about the View it originated from (source), and various attributes about the key that was pressed or released.

Key events are consumable. This means any observer can call consume on the event and prevent subsequent listeners from receiving it.

package keyboard import io.nacular.doodle.core.View import io.nacular.doodle.event.KeyListener.Companion.pressed fun consume(view: View) { //sampleStart view.keyChanged += pressed { event -> // ... take action based on event event.consume() // indicate that no other listeners should be notified } //sampleEnd }

Virtual keys and text

KeyEvent's key is a layout independent identifier that tells you which "virtual key" was pressed or which text the key can be translated into. Most key handling use-cases should use this property to compare keys.

package keyboard import io.nacular.doodle.core.View import io.nacular.doodle.event.KeyListener.Companion.pressed import io.nacular.doodle.event.KeyText.Companion.Backspace import io.nacular.doodle.event.KeyText.Companion.Enter fun virtualKeys(view: View) { //sampleStart view.keyChanged += pressed { event -> when (event.key) { Enter -> { /* ... */ } Backspace -> { /* ... */ } // ... } } view.keyChanged += pressed { event -> // this will be user-appropriate text when the key pressed is not // one of the "named" keys (i.e. Tab, Shift, Enter, ...) event.key.text } //sampleEnd }

Physical keys

Some applications will require the use of "physical" keys instead of virtual ones. This makes sense for games or other apps where the key position on a physical keyboard matters.

This information comes from KeyEvent's code.

package keyboard import io.nacular.doodle.core.View import io.nacular.doodle.event.KeyCode.Companion.AltLeft import io.nacular.doodle.event.KeyCode.Companion.AltRight import io.nacular.doodle.event.KeyCode.Companion.Backspace import io.nacular.doodle.event.KeyListener.Companion.pressed fun physicalKeys(view: View) { //sampleStart view.keyChanged += pressed { event -> when (event.code) { AltLeft -> { /* ... */ } AltRight -> { /* ... */ } Backspace -> { /* ... */ } // ... } } //sampleEnd }
caution

Physical keys do not take keyboard differences and locale into account; so avoid them if possible

Event sinking and filtering

Pointer and Key events "sink" from ancestors down to their target. The "sink" portion is the first phase of event handling; and it runs before the "bubbling" phase. The root ancestor and all descendants toward the target View are notified of the event before the target is during this phase.

This phase can also be considered the filter phase, because it lets ancestors decide which events their children get to handle, since they can "veto" an event before it reaches the intended target.

Listeners can take part in this phase of event handling as well. You do this by registering the following types via their respective properties within View.

package pointerinput import io.nacular.doodle.core.View import io.nacular.doodle.event.KeyListener.Companion.pressed import io.nacular.doodle.event.PointerListener.Companion.clicked import io.nacular.doodle.event.PointerMotionListener.Companion.moved fun filter(view: View) { //sampleStart view.pointerFilter += clicked { event -> // called whenever a pointer is pressed on this // View or its children, before the target // is notified } view.pointerMotionFilter += moved { event -> // called whenever a pointer is pressed on this // View or its children, before the target // is notified } view.keyFilter += pressed { event -> // called whenever a key is pressed while this // View or one of its children has focus, // before the target is notified } //sampleEnd }
info

Calling consume during filter will prevent descendants (and the target) from receiving the event

Event bubbling

Event "bubbling" is the second and final phase of event handling. This phase notifies the target View and then "bubbles" the event up to ancestors of that View as long as the event remains unconsumed. This means you can listen to all events that happen to the descendants of a View during this phase as well. And similar to the filter phase, you will only receive events that were not consumed before-hand, which in this case means down the hierarchy.

info

Bubbling is canceled if any listener calls consume.

Pointer events for ancestors

Pointer events sent to an ancestors and descendants are slightly different from those sent to the View. These events continue to have the same target (View where the event fired), but their source changes to the recipient ancestor as they bubble or sink.