Form controls
Back on Forms, you built one from scratch. This page is the reference for what to actually put inside it: every built-in control, organized by the type of value it produces rather than by name — handy when you know you need "something that picks a ClosedRange<T>" but haven't settled on which control does that yet. They're all functions returning a FieldVisualizer, and they all live in the io.nacular.doodle.controls.form package.
These controls still need a Behavior explicitly installed, same as any other control. Everything below assumes BasicTheme or NativeTheme (Web, Desktop).
At a glance:
| Produces | Controls |
|---|---|
Boolean | check, switch |
T (interpolatable) | slider, circularSlider |
ClosedRange<T> (interpolatable) | rangeSlider, circularRangeSlider |
T | textField, radioList, singleChoiceList, spinButton, selectBox, form |
T? | optionalRadioList, optionalSingleChoiceList, optionalSelectBox |
List<T> | list, checkList, switchList |
LocalFile | file |
List<LocalFile> | files |
| (structural) | labeled, scrolling, framed |
Doodle has many built-in controls that cover a range of data types for forms. These controls come as functions that return FieldVisualizers. They are all found within the io.nacular.doodle.controls.form package.
Usage of these controls still require explicit installation of Behaviors. Those shown below use behaviors from the BasicTheme orNativeTheme (Web, Desktop).
Boolean
check
A check-box representing a true/false value — Type: Boolean, View: CheckBox.
package forms
import io.nacular.doodle.controls.form.check
fun check() {
//sampleStart
check("Some question")
//sampleEnd
}
switch
Same true/false value, presented as a sliding switch instead — Type Boolean, View: Switch.
package forms
import io.nacular.doodle.controls.form.switch
fun switch() {
//sampleStart
switch("Some question")
//sampleEnd
}
T (interpolatable)
slider
A bounded value on a straight-line slider — Type T, View: Slider<T>.
package forms
import io.nacular.doodle.controls.form.slider
fun slider() {
//sampleStart
slider(0 .. 100)
//sampleEnd
}
circularSlider
The same bounded value, on a circular slider instead — Type T, View: CircularSlider<T>.
package forms
import io.nacular.doodle.controls.form.circularSlider
fun circularSlider() {
//sampleStart
circularSlider(0 .. 100)
//sampleEnd
}
ClosedRange<T> (interpolatable)
rangeSlider
A bounded range, rather than a single value — Type ClosedRange<T>, View: RangeSlider<T>.
package forms
import io.nacular.doodle.controls.form.rangeSlider
fun rangeSlider() {
//sampleStart
rangeSlider(0 .. 100)
//sampleEnd
}
circularRangeSlider
The circular version of the same bounded range — Type ClosedRange<T>, View: CircularRangeSlider<T>.
package forms
import io.nacular.doodle.controls.form.circularRangeSlider
fun circularRangeSlider() {
//sampleStart
circularRangeSlider(0 .. 100)
//sampleEnd
}
T
textField
Generates a value of any type T from text input — Type T, View: TextField.
package forms
import io.nacular.doodle.controls.form.textField
fun textField() {
//sampleStart
textField(pattern = Regex(".+"))
//sampleEnd
}
radioList
Requires exactly one item selected from a list of buttons — Type T, View: container of RadioButtons.
package forms
import io.nacular.doodle.controls.form.radioList
fun radioList() {
//sampleStart
radioList("Item1", "Item2", "Item3")
//sampleEnd
}
singleChoiceList
Requires exactly one item selected from a List — Type T, View: List<T>.
package forms
import io.nacular.doodle.controls.form.singleChoiceList
fun singleChoiceList() {
//sampleStart
singleChoiceList("Item1", "Item2", "Item3")
//sampleEnd
}
spinButton
Requires exactly one item selected by stepping through — Type T, View: SpinButton.
package forms
import io.nacular.doodle.controls.form.spinButton
fun spinButton() {
//sampleStart
spinButton("Item1", "Item2", "Item3")
//sampleEnd
}
selectBox
Requires exactly one item selected from a dropdown — Type T, View: SelectBox.
package forms
import io.nacular.doodle.controls.form.selectBox
fun selectBox() {
//sampleStart
selectBox("Item1", "Item2", "Item3")
//sampleEnd
}
form
A sub-form that collects arbitrary fields and maps them to a single, strongly-typed value — Type T, View: Form. See Forms as fields for a worked example.
package forms
import io.nacular.doodle.controls.form.form
import io.nacular.doodle.controls.form.labeled
import io.nacular.doodle.controls.form.map
import io.nacular.doodle.controls.form.textField
import io.nacular.doodle.utils.ToStringIntEncoder
fun formField() {
data class Person(val name: String, val age: Int)
//sampleStart
form<Person> { this(
initial.map { it.name } to labeled("Name") { textField( ) },
initial.map { it.age } to labeled("Age" ) { textField(encoder = ToStringIntEncoder) },
onInvalid = {}
) { name, age ->
Person(name, age) // construct result when valid
} }
//sampleEnd
}
T?
Same three list-based controls as above, in optional form — each allows selecting zero or one item rather than requiring exactly one:
optionalRadioList
Type T?, View: container of RadioButtons.
package forms
import io.nacular.doodle.controls.form.optionalRadioList
fun optionalRadioList() {
//sampleStart
optionalRadioList("Item1", "Item2", "Item3")
//sampleEnd
}
optionalSingleChoiceList
Type T?, View: List<T>.
package forms
import io.nacular.doodle.controls.form.optionalSingleChoiceList
fun optionalSingleChoiceList() {
//sampleStart
optionalSingleChoiceList("Item1", "Item2", "Item3")
//sampleEnd
}
optionalSelectBox
Type T?, View: SelectBox.
package forms
import io.nacular.doodle.controls.form.optionalSelectBox
fun optionalSelectBox() {
//sampleStart
optionalSelectBox("Item1", "Item2", "Item3", unselectedLabel = "")
//sampleEnd
}
List<T>
list
Allows selecting zero or more items — Type List<T>, View: List<T>.
package forms
import io.nacular.doodle.controls.form.list
fun list() {
//sampleStart
list("Item1", "Item2", "Item3")
//sampleEnd
}
Ctrl/Meta-click deselects items in the list when it uses BasicListBehavior
checkList
The same zero-or-more selection, presented as a list of check-boxes — Type List<T>, View: container of CheckBoxex.
package forms
import io.nacular.doodle.controls.form.check
fun check() {
//sampleStart
check("Some question")
//sampleEnd
}
switchList
The same again, this time as a list of switches — Type List<T>, View: Container of Switches.
package forms
import io.nacular.doodle.controls.form.switchList
fun switchList() {
//sampleStart
switchList("Item1", "Item2", "Item3")
//sampleEnd
}
LocalFile
file
Represents a single local file — Type LocalFile, View: FileSelector.
package forms
import io.nacular.doodle.controls.form.file
import io.nacular.doodle.datatransport.PlainText
fun file() {
//sampleStart
file(acceptedTypes = setOf(PlainText))
//sampleEnd
}
List<LocalFile>
files
The same idea, for a whole list of local files at once — Type List<LocalFile>, View: FileSelector.
package forms
import io.nacular.doodle.controls.form.files
import io.nacular.doodle.datatransport.PlainText
fun files() {
//sampleStart
files(acceptedTypes = setOf(PlainText))
//sampleEnd
}
Structure
These three don't produce a new type of value at all — they wrap an existing control to change how it's presented or laid out.
labeled
Wraps a control and gives it a name — Type T, View: Container. This is what every example on the Forms page uses to attach labels like "Name" and "Age" to otherwise-unlabeled fields.
package forms
import io.nacular.doodle.controls.form.field
import io.nacular.doodle.controls.form.labeled
import io.nacular.doodle.core.view
fun <T> labeled() {
//sampleStart
labeled("Name", help = "Help text") {
field<T> {
view {}
}
}
//sampleEnd
}
scrolling
Wraps a control in a scrollable region — Type T, View: ScrollPanel.
package forms
import io.nacular.doodle.controls.form.field
import io.nacular.doodle.controls.form.scrolling
import io.nacular.doodle.core.View.Companion.fixed
import io.nacular.doodle.core.view
import io.nacular.doodle.geometry.Size
fun <T> scrolling() {
//sampleStart
scrolling {
scrollPanel.preferredSize = fixed(Size(100))
field<T> {
view {}
}
}
//sampleEnd
}
framed
Wraps a control so it can be styled and laid out as its own unit — Type T, View: Container.
package forms
import io.nacular.doodle.controls.form.field
import io.nacular.doodle.controls.form.framed
import io.nacular.doodle.core.view
import io.nacular.doodle.drawing.Color.Companion.Lightgray
import io.nacular.doodle.drawing.paint
import io.nacular.doodle.layout.Insets
fun <T> framed() {
//sampleStart
framed {
// configure the container builder
insets = Insets(10.0)
render = {
rect(bounds.atOrigin, radius = 10.0, fill = Lightgray.paint)
}
field<T> {
view {}
}
}
//sampleEnd
}
Custom Controls
None of the above fit? Building your own is exactly as easy as creating a field directly — implement FieldVisualizer, or reach for the field DSL. Every control is really just a binding between a Field, its initial value, and a View.
Here's a Boolean field built from scratch, pairing a Label with a Switch:
package forms
import io.nacular.doodle.controls.buttons.Switch
import io.nacular.doodle.controls.form.Form.Valid
import io.nacular.doodle.controls.form.field
import io.nacular.doodle.controls.form.ifValid
import io.nacular.doodle.controls.text.Label
import io.nacular.doodle.core.container
import io.nacular.doodle.geometry.Size
import io.nacular.doodle.layout.constraints.constrain
import io.nacular.doodle.text.AnnotatedString
import io.nacular.doodle.utils.TextAlignment.Start
//sampleStart
fun switchField(text: AnnotatedString) = field {
container {
+ Label(text).apply {
textAlignment = Start
}
+ Switch().apply {
initial.ifValid { selected = it } // adopt initial value if present
selectedChanged += { _,_,_ ->
state = Valid(selected) // update field as switch changes
}
suggestSize(Size(40, 25))
state = Valid(selected) // ensure field is valid at beginning
}
focusable = false // ensure wrapping container isn't focusable
layout = constrain(children[0], children[1]) { label, switch ->
switch.left eq parent.right - 10 - readonly(switch.width)
switch.centerY eq parent.centerY
label.left eq 10
label.right eq switch.left - 10
label.centerY eq switch.centerY
}
}
}
//sampleEnd
This is a similar example building a color-strip control from scratch.
package forms
import io.nacular.doodle.controls.form.Form
import io.nacular.doodle.docs.utils.BlueColor
import io.nacular.doodle.docs.utils.colorStrip
import io.nacular.doodle.drawing.Color.Companion.Black
import io.nacular.doodle.drawing.Color.Companion.Blue
import io.nacular.doodle.drawing.Color.Companion.Green
import io.nacular.doodle.drawing.Color.Companion.Red
fun colorStripUsage() {
//sampleStart
Form { this(
Green to colorStrip(Red, Green, Blue, BlueColor, Black),
onInvalid = {}
) { color ->
println("Form valid: $color")
} }
//sampleEnd
}
package io.nacular.doodle.docs.utils
import io.nacular.doodle.controls.buttons.ButtonGroup
import io.nacular.doodle.controls.buttons.ToggleButton
import io.nacular.doodle.controls.form.Form.Valid
import io.nacular.doodle.controls.form.field
import io.nacular.doodle.controls.form.ifValid
import io.nacular.doodle.controls.theme.simpleButtonRenderer
import io.nacular.doodle.core.container
import io.nacular.doodle.drawing.Color
import io.nacular.doodle.drawing.lighter
import io.nacular.doodle.drawing.rect
import io.nacular.doodle.drawing.stroke
import io.nacular.doodle.geometry.Size
import io.nacular.doodle.geometry.inset
import io.nacular.doodle.layout.HorizontalFlowLayout
//sampleStart
fun colorStrip(first: Color, second: Color, vararg rest: Color, size: Double = 30.0) = field {
container {
val buttonGroup = ButtonGroup()
+ listOf(first, second, *rest).map { color ->
// use toggle button since it has desirable selection behavior
ToggleButton().apply {
suggestSize(Size(size))
behavior = simpleButtonRenderer { button, canvas ->
val radius = width * 0.2
val thickness = 2.0
val fillColor = if (button.model.pointerOver) color.lighter() else color
when {
selected -> canvas.rect(
rectangle = bounds.atOrigin.inset(thickness / 2),
radius = radius,
color = fillColor,
stroke = color.inverted.stroke(thickness = thickness)
)
else -> canvas.rect(
rectangle = bounds.atOrigin,
radius = radius,
color = fillColor
)
}
}
acceptsThemes = false
selectedChanged += { _,_,_ ->
if (selected) {
state = Valid(color) // update field state to selected
}
}
initial.ifValid {
selected = it == color // adopt initial value if it matches something in the list
}
buttonGroup += this // ensure only 1 item is ever selected
}
}
layout = HorizontalFlowLayout(spacing = 4.0)
focusable = false // ensure wrapping container isn't focusable
}
}
//sampleEnd