Skip to main content

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.

info

These controls still need a Behavior explicitly installed, same as any other control. Everything below assumes BasicTheme or NativeTheme (Web, Desktop).

At a glance:

ProducesControls
Booleancheck, switch
T (interpolatable)slider, circularSlider
ClosedRange<T> (interpolatable)rangeSlider, circularRangeSlider
TtextField, radioList, singleChoiceList, spinButton, selectBox, form
T?optionalRadioList, optionalSingleChoiceList, optionalSelectBox
List<T>list, checkList, switchList
LocalFilefile
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.

info

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 }
tip

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