Skip to main content

UI components

Doodle has several UI components in the controls library that range from the simple (buttons, text-fields), to the complex (like lists and carousels). Below is a selection of the most common ones.

Library Required

You will need to add the Controls library to your app's dependencies.

build.gradle.kts

dependencies { implementation ("io.nacular.doodle:controls:$doodleVersion") }
info

Most of these components rely entirely on their Behavior for rendering. And, they do not implement default behavior themselves to minimize bundle size. So you need to specify behaviors explicitly or use a Theme that provides them for the controls you use.

Label

Holds and displays text with support for basic styling.

Labels can also have StyledText, word wrapping, vertical and horizontal alignment, and change how their letters and lines are spaced.

TextField

Provides simple (un-styled) text input that is highly customizable. You can specify each field's purpose as well to enable platform-specific treatment for things like email, numbers, telephone, etc..

TextFields can also be customized using NativeTextFieldBehaviorModifier. See the code sample for how this is achieved.

PushButton

A component that triggers an action when pressed; usually with the pointer or keyboard.

ToggleButton

A button component that toggles between 2 states when pressed; usually with the pointer or keyboard.

CheckBox

A toggle component that represents an on/off state and is triggered when pressed; usually with the pointer or keyboard.

RadioButton

A toggle component that represents an on/off state and is triggered when pressed; usually with the pointer or keyboard. RadioButtons are typically used in lists with ButtonGroup to represent the selection of a single item from this list.

Switch

A toggle component that triggers an action when selected; usually with the pointer or keyboard.

Control that opens a url when triggered.

HyperLinks are also fully customizable with NativeHyperLinkStyler. This lets you use an arbitrary behavior, while retaining the core functionality to open urls. See the code sample for how this is achieved.

FileSelector

A toggle component that triggers an action when selected; usually with the pointer or keyboard. The files (a list of LocalFiles that) selected by the user are available as an event via the filesLoaded property.

FileSelectors can also be customized using NativeFileSelectorStyler. See the code sample for how this is achieved.

Photo

Images in Doodle are not Views, they are more like text, in that you render them directly to a Canvas. The Photo component provides a simple wrapper around an Image.

LazyPhoto

Experimental

This control relies on experimental Coroutine features.

LazyPhoto is like Photo, except it takes a Deferred<Image> instead of an Image. This allows apps to map loading images to Views even if those images are still pending. LazyPhoto also offers full customization of how it renders during the loading state. You provide a lambda that is called for all rendering while it is pending.

You can go a step further and animate the loading state using Animator and re-rendering the LazyPhoto when new frames are needed to be displayed. This app shows how you might achieve this.

ProgressBar

Represents a value within a specified range that usually indicates progress toward some goal. It provides notifications when its value or range changes. Specify a range by passing a ClosedRange or ConfinedValueModel in the constructor.

ProgressBar is a specialization of ProgressIndicator, which should be used for more generalized progress display (i.e. circular)

ProgressIndicators can also take different shapes. Here's an example that uses basicCircularProgressIndicatorBehavior.

This one draws a path using PathProgressIndicatorBehavior.

Slider

Slider holds a strongly typed value within a specified range and allow the user to change the value. It provides notifications when its value or range changes. Specify a range by passing a ClosedRange or ConfinedValueModel in the constructor.

You can also confine the values to a predefined set within the range by specifying the ticks count and setting snapToTicks to true. This will pin the slider values to an evenly spaced set of points along its range.

tip

All Sliders (including CircularSlider, RangeSlider, and CircularRangeSlider) are strongly typed. Which means you can create Integer sliders that snap to each integer value. Therefore, it is not necessary to specify ticks and snapToTicks to ensure they only land on whole numbers.

It is possible to still restrict their range further using these properties however. Then, they will only take on integer values that match the tick count.

CircularSlider

CircularSlider behaves just like a regular Slider, except it is meant to be a ring. This means it provides notifications when its value or range changes and these can be specified by passing a ClosedRange or ConfinedValueModel in the constructor.

Like Slider, you can also confine the values to a predefined set within the range by specifying the ticks count and setting snapToTicks to true. This will pin the slider values to an evenly spaced set of points along its range.

RangeSlider

RangeSlider holds a strongly typed inner range within a specified outer range and allow the user to change these values. It provides notifications when its either changes. Specify the ranges by passing ClosedRanges or a ConfinedRangeModel in the constructor.

You can also confine the inner range using a ticks count and setting snapToTicks to true, just like regular ranges. This will pin the values of the inner range to an evenly spaced set of points along its range.

CircularRangeSlider

CircularRangeSlider behaves just like a regular RangeSlider, except it is meant to be a ring.

Like RangeSlider, you can also confine the values to a predefined set within the range by specifying the ticks count and setting snapToTicks to true. This will pin the slider values to an evenly spaced set of points along its range.

Spinner

Spinner is a list data structure analog that lets you represent a list of items where only one is visible (selected) at a time. They work well when the list of options is relatively small, or the input is an incremental value: like the number of items to purchase.

Spinner takes a SpinnerModel that works like an Iterator. This allows them to represent an open-ended list of items that do not need to be loaded up front.

Dropdown is a list data structure similar to Spinner. It also lets you represent a list of choices where only one is visible (selected) at a time.But unlike a Spinner, the choices are shown in a list when the control is activated. They work well when the list of options is relatively small.

Dropdown takes a ListModel that works like an Iterator. This allows them to represent an open-ended list of items that do not need to be loaded up front.

Menu is a View that contains a vertical list of interactive items. These items notify whenever the user interacts with them using a pointer or the keyboard. This control support a couple types of action items and one that shows a new "sub" menu when the user interacts with it. You specify the contents of a Menu using the MenuFactory, which provides a declarative DSL for defining the structure and behavior of Menus.

Note that the Menu implementation does not handle showing it as a popup. But this is easy to create using the PopupManager or ModalManager. The following app does just this. It has a button that shows a menu as a modal when clicked.

Module Required

You must include the MenuFactoryModule in your application in order to use these features.

package controls import io.nacular.doodle.application.Application import io.nacular.doodle.application.Modules.Companion.MenuFactoryModule import io.nacular.doodle.application.application import io.nacular.doodle.controls.popupmenu.MenuFactory import io.nacular.doodle.core.Display import org.kodein.di.instance class MenuApp(display: Display, menus: MenuFactory): Application { override fun shutdown() {} } fun menusModule() { //sampleStart application(modules = listOf(MenuFactoryModule)) { MenuApp(display = instance(), menus = instance()) } //sampleEnd }

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

tip

Move the button around to see how the Menu adjusts the location of its popups to keep them visible as much as possible.

tip

Menus work really well when shown as modals via the ModalManager

StarRater

A highly customizable control that displays a rating between [0, n] using stars. It also lets the user change the underlying value.

List

The List control is a visual analog to the list data structure. It is a readonly, ordered, generic collection of items with random access to its members.

You need 2 things to create a List: a ListModel, and ItemVisualizer.

tip

You also need to provide a ListBehavior or use a Theme with one since List delegates rendering. The examples below use BasicListBehavior which is also available as a module within BasicTheme.

The model represents the data within the List, and the visualizer provides a way to translate each item to a View that will be rendered within the List.

Lists provide memory optimization by only rendering the contents within their viewport, recycling items to display new rows. The default setting caches 10 extra items; but this can be changed with the scrollCache property when creating the List.

The following shows a DynamicList of countries (a custom data class). These Lists are useful when the underlying model can change after creation. This demo loads images asynchronously and adds new countries to the model as they load. The demo also illustrates a custom visualizer that represents each country as a name label and flag image.

Icons made by Freepik

This List displays a set of countries, with each having a name and flag image. A DynamicList is used here because the underlying model changes as each country is added asynchronously when its image loads.

tip

DynamicList is readonly (though its models may change), while MutableList is read/write.

The Carousel control is a visual analog to the list data structure. It is a readonly (see DynamicCarousel if you want one that responds to changes in a DynamicListModel), ordered, generic collection of items with random access to its members. It provides memory optimization by only rendering the contents displayed by its Presenter, and recycling views as it scrolls. The result is that Carousels can hold extremely large data sets without impacting performance.

You need 3 things to create a Carousel: a ListModel, ItemVisualizer and CarouselBehavior. The model represents the data within the Carousel, and the visualizer provides a way to translate each item in the model to a View that will be rendered within the Carousel. The behavior provides a Presenter and Transitioner, both of which are required to render the contents of a Carousel.

Carousels are very flexible containers that can be fully customized via their Presenter and Transitioner.

  • Presenters decide what items are shown in the Carousel and specify their bounds, transform, opacity, zOrder etc..
  • Transitioners control how a Carousel moves between items.

Adjust the presenter used for the Carousel above to see examples of some very different behaviors that can be achieved. This demo uses the following built-in Presenters:

tip

Presenters can also add Supplemental Views to the Carousel to support the items generated from the model. The ReflectionPresenter does this to represent the "floor" where the items are reflected.

The following shows a simple calendar built with a Carousel. The data model is a list of LocalDate values at the start of each month. In this case a total of 10 years into the future is represented. The LinearPresenter is used with constraints that show 2 months side-by-side.

Tree

The Tree control is a visual analog to the tree data structure. It is a readonly, hierarchical, generic collection of items that are accessible via a numeric path.

You need 2 things to create a Tree: a TreeModel, and ItemVisualizer.

tip

You also need to provide a Behavior or use a Theme with one since Tree delegates rendering.

tip

DynamicTree is readonly (though its models may change), while MutableTree is read/write.

TreeTable

The TreeTable is very similar to a Tree, except it shows a structured set of fields from each item as columns, like a Table.

You need 2 things to create a Tree: a TreeModel, and ItemVisualizer.

tip

You also need to provide a Behavior or use a Theme with one since Tree delegates rendering.

Table

A Table is very similar to a List (readonly analog to the list data structure). It is like a List that can display structured data for each entry they hold. It is also strongly typed and homogeneous, like List. So each item is of some type <T>. The values of each column are therefore derivable from each <T> in the table. The Table below contains a list of Person and has columns for the name, age, and attending (whether they are attending an event). Columns can also produce arbitrary values, which is done to show the index of each item.

Each column's CellVisualizer ultimately controls what is displayed in it. The visualizer is given the value of each element in that column to produce a View. So the Name column gets a String, while the Attending column gets a Boolean. The first column has values of type Unit, and uses the RowNumberGenerator to display the index of each item.

tip

DynamicTable supports changes to its model, and MutableTable allows editing.

MutableTable

This is a table that can modify its underlying model. That means the table can do CRUD operations or sorting that will modify the model. This example shows sorting.

KeyValueTable

This is a table that represents a key-value map of data. The table only has two columns: one for the key and value in each pair.

GridPanel

This control manages a generic list of Views and displays them within a grid layout. Items can be added to or removed from the panel. Each item added indicates the row/column it sits at and the number of rows / columns it spans. This, along with the rowSizingPolicy and columnSizingPolicy control how the items are ultimately laid out.

SplitPanel

This control divides a region into two areas, each occupied by a View. It also allows the user to change the portion of its viewport dedicated to either view.

This shows how you might nest horizontal and vertical SplitPanels.

tip

Requires a SplitPanelBehavior for rendering. BasicTheme provides one.

TabbedPanel

This control manages a generic list of items and displays them one at a time using an ItemVisualizer. Each item is generally tracked with a visual "tab" that allows selection of particular items in the list.

The panel takes 2 visualizers; one to convert each item to a View that will be displayed as the tab, and another to convert each item to the main tab content. The View returned from the main visualizer will be scaled to fit the TabPanel when using basicTabbedPanelBehavior. In this example, we use a ScrollPanelVisualizer for the main content so it turns into a ScrollPanel with our view embedded. That works well for TabbedPanels that are meant to display arbitrary Views.

tip

This control requires a TabbedPanelBehavior for rendering. This demo uses the basicTabbedPanelBehavior module which installs BasicTabbedPanelBehavior

ColorPicker

This control allows a user to pick an RGB color by specifying a hue and opacity.

MonthPanel

This control displays the days of a given month. It does not display a header with the day of the week though. This functionality is provided separately in the DaysOfTheWeekPanel. This simplifies reuse as a core component of calendars. Excluding the header means the MonthPanel can be used in vertically scrolling calendars where the days are pinned to the top. Or in horizontal setups where the days are attached to it (within a container that has both panels).

The panel can either show or hide days in the adjacent months using showAdjacentMonths. It can also start at any day of the week via the weekStart property.

DaysOfTheWeekPanel

This control is meant as a header for the MonthPanel. It shows days of the week starting at the given weekStart property.

Custom Calendar

This shows the MonthPanel and DaysOfTheWeekPanel being used to create a simple vertical calendar with one column. This calendar uses a List<LocalDate> with a model that contains the months of the current year. Each date in the list is visualized using a custom View that simply holds a label and MonthPanel. These are updated as the list scrolls and items are recycled.

The custom View provides a visualizer to the MonthPanel that controls the colors for each day as well as the background selection rendering.

Form

Forms provide a way of collecting structured data from a user. This is generally quite complex given the wide range of visual representations, data types, and validation steps usually involved. Doodle simplifies this entire flow with a single control that offers full customization and type safety.

This example shows the use of validating text inputs, a radio list, and a sub form to gather some data about a person.

tip

Form fields can bind to any type and use any View for display. This is done using a FieldVisualizer or the field dsl.