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
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.
You can automatically style Views using Themes