Skip to main content

Behaviors

It is common to make a View's behavior and presentation configurable. In many cases this happens through properties like colors, fonts, etc.

package rendering import io.nacular.doodle.controls.text.TextField import io.nacular.doodle.drawing.Color.Companion.Darkgray import io.nacular.doodle.drawing.Color.Companion.White fun example() { //sampleStart val textField = TextField().apply { backgroundColor = Darkgray foregroundColor = White borderVisible = false } //sampleEnd }

Sometimes a View needs to support more complex customization. Take a TabbedPanel for example. The number of configurations is fairly open-ended; and the API would be needlessly complex if it tried to encompass everything.

This is where a Behavior comes in handy. Views can offer deep customization by delegating rendering, hit detection and anything else to Behaviors. TabbedPanel--along with TextField and many other controls--actually does this.

Implementing a Behavior

Behaviors offer a few common capabilities that help with View customization. You create one by implementing the Behavior interface, or a sub-type of it depending on the target View.

package rendering import io.nacular.doodle.controls.buttons.Button import io.nacular.doodle.core.Behavior import io.nacular.doodle.drawing.Canvas import io.nacular.doodle.geometry.Point //sampleStart class MyBehavior: Behavior<Button> { override fun install (view: Button ) {} override fun render (view: Button, canvas: Canvas) {} override fun contains (view: Button, point : Point ) = point in view.bounds override fun clipCanvasToBounds (view: Button ) = true override fun mirrorWhenRightToLeft(view: Button ) = view.mirrorWhenRightLeft override fun uninstall (view: Button ) {} } //sampleEnd
tip

The methods on Behavior are all optional

Behaviors support installation and uninstallation to and from Views. This gives each Behavior a chance to configure the target View upon first assignment and cleanup when removed.

Delegating to a Behavior

View subtypes need to manage behaviors directly. Kotlin does not have self types, so the View base class cannot have a behavior<Self> to make this easier.

package rendering import io.nacular.doodle.core.Behavior import io.nacular.doodle.core.View import io.nacular.doodle.core.behavior //sampleStart class AView: View() { // ... var behavior: Behavior<AView>? by behavior() } //sampleEnd

However, View subtypes can use the behavior delegate to guarantee proper installation and uninstallation. This delegate also ensures a Behavior's overrides for things like clipCanvasToBounds or mirrorWhenRightToLeft are not missed during installation.

Specialized Behaviors

As mentioned before, TabbedPanel delegates a lot to its Behavior. It actually exposes the fact that it is a container to it. This is done using the TabbedPanelBehavior sub interface. Classes that implement this interface are able to directly modify their panel's children and layout.

package rendering import io.nacular.doodle.controls.panels.TabbedPanel import io.nacular.doodle.controls.panels.TabbedPanelBehavior import io.nacular.doodle.core.Layout.Companion.simpleLayout import io.nacular.doodle.core.view import io.nacular.doodle.utils.diff.Differences //sampleStart class MyTabbedPanelBehavior: TabbedPanelBehavior<Any>() { override fun install(view: TabbedPanel<Any>) { // children and layout accessible to TabbedPanelBehavior subclasses view += view {} view.layout = simpleLayout { // ... } } override fun uninstall(view: TabbedPanel<Any>) { view.children.clear() view.layout = null } override fun itemsChanged(panel: TabbedPanel<Any>, differences: Differences<Any>) { // ... } override fun selectionChanged(panel: TabbedPanel<Any>, new: Any?, newIndex: Int?, old: Any?, oldIndex: Int?) { // ... } } fun usage(tabbedPanel: TabbedPanel<Any>) { tabbedPanel.behavior = MyTabbedPanelBehavior() } //sampleEnd

This provides great flexibility when defining the presentation and behavior for TabbedPanels. You can do similar things with Views in your app.

tip

You can automatically style Views using Themes