Panels & Layout
Coming from Data Display, this page is a little different from the rest — every control here exists to arrange other Views, rather than show a value of its own.
Which one do you actually want?
| If you need... | Reach for |
|---|---|
| A View too big for its space, scrollable rather than resized | ScrollPanel |
| A grid of rows and columns | GridPanel |
| Two resizable areas, side by side | SplitPanel |
| One of several Views visible at a time, tab-style | TabbedPanel |
| One of several sections visible at a time, vertically stacked | Accordion |
All five of these are really just pre-built Layouts for common arrangements. If your layout doesn't fit any of them — an irregular arrangement, say — writing constraints by hand is still completely reasonable. These controls exist to save you the trouble for the shapes that come up constantly, not to be the only way to arrange Views.
ScrollPanel
The simplest of the bunch: wraps a single content View, clips it to the panel's bounds, and scrolls it when it's too big to fit — rather than squeezing or resizing it the way GridPanel or SplitPanel might. Set content any time, and contentChanged notifies you when it does.
package controls
import io.nacular.doodle.controls.panels.ScrollPanel
import io.nacular.doodle.core.View
import io.nacular.doodle.layout.constraints.ideal
fun scrollPanel(bigView: View) {
//sampleStart
val panel = ScrollPanel(content = bigView).apply {
contentConstraints = {
it.width eq parent.width - verticalScrollBarWidth
it.height hint ideal
}
}
//sampleEnd
}
A property worth knowing about control how the content behaves as the panel itself resizes: contentConstraints — it is a small function returning a constraint, so you can pin the content to the panel's exact width while letting its height scroll freely, say.
This is exactly what powers the scrolling form control from Form Controls, and it's what TabbedPanel's ScrollPanelVisualizer (just below) wraps tab content in.
nativeScrollPanelBehavior (Web, Desktop)
GridPanel
Arranges children into a grid of rows and columns — the control version of the constraint-based grids you'd otherwise build by hand on the Constraints page, packaged up for the common case. Each item you add declares the row and column it sits at, plus how many rows and columns it spans — a 2×1 item to span two columns, say. Those spans, together with the panel's rowSizingPolicy and columnSizingPolicy, are what ultimately decide how everything gets sized.
- Demo
- Usage
package controls
import io.nacular.doodle.controls.panels.GridPanel
import io.nacular.doodle.controls.panels.GridPanel.Companion.FitPanel
import io.nacular.doodle.core.View
import io.nacular.doodle.geometry.Size
import io.nacular.doodle.utils.Resizer
fun gridPanel(
view1: View,
view2: View,
view3: View,
view4: View,
view5: View,
view6: View,
view7: View
) {
//sampleStart
val panel = GridPanel().apply {
rowSpacing = { 10.0 }
columnSpacing = { 10.0 }
rowSizingPolicy = FitPanel // FitContent, or custom policy
columnSizingPolicy = FitPanel // FitContent, or custom policy
add(view1, columnSpan = 2 ) // defaults to row = 0, col = 0
add(view2, row = 1, column = 0)
add(view3, row = 1, column = 1)
add(view4, row = 2, column = 2)
add(view5, row = 0, column = 2, rowSpan = 2)
add(view6, row = 2, column = 0)
add(view7, row = 2, column = 1)
suggestSize(Size(200))
Resizer(this, movable = false)
}
//sampleEnd
}SplitPanel
Divides a region into two areas, each holding one View, with a draggable divider letting the user resize the split between them. Nest one inside another to build more complex arrangements — a horizontal split whose second half is itself a vertical split, for instance.
- Demo
- Horizontal
- Usage
package controls
import io.nacular.doodle.controls.Photo
import io.nacular.doodle.controls.panels.ScrollPanel
import io.nacular.doodle.controls.panels.SplitPanel
import io.nacular.doodle.docs.utils.CircularView
import io.nacular.doodle.geometry.Size
import io.nacular.doodle.image.Image
import io.nacular.doodle.layout.Insets
import io.nacular.doodle.utils.Orientation.Vertical
fun splitPanel(image: Image) {
//sampleStart
val panel = SplitPanel(orientation = Vertical /*| Horizontal*/).apply {
suggestSize(Size(500, 300))
firstItem = ScrollPanel(CircularView(250.0))
lastItem = Photo(image)
ratio = 1f / 3
insets = Insets(2.0)
}
//sampleEnd
}This shows how you might nest horizontal and vertical SplitPanels.
- Demo
- Usage
package controls
import io.nacular.doodle.controls.SingleItemSelectionModel
import io.nacular.doodle.controls.StringVisualizer
import io.nacular.doodle.controls.invoke
import io.nacular.doodle.controls.list.List
import io.nacular.doodle.controls.panels.ScrollPanel
import io.nacular.doodle.controls.panels.SplitPanel
import io.nacular.doodle.docs.utils.panel
import io.nacular.doodle.drawing.TextMetrics
import io.nacular.doodle.geometry.Size
import io.nacular.doodle.utils.Orientation.Horizontal
import io.nacular.doodle.utils.Resizer
fun nestedSplitPanel(textMetrics: TextMetrics) {
//sampleStart
val list = List(1..10, StringVisualizer(), SingleItemSelectionModel()).apply {
cellAlignment = { it.left eq 10 }
}
val main = panel(textMetrics, "1", shadow = null, cornerRadius = 0.0)
val info = panel(textMetrics, "info", shadow = null, cornerRadius = 0.0)
val panel = SplitPanel(orientation = Horizontal).apply {
suggestSize(Size(500, 300))
firstItem = SplitPanel().apply {
firstItem = ScrollPanel(list).apply {
contentConstraints = { it.width eq parent.width; it.height eq it.idealWidth }
}
lastItem = main
ratio = 1f / 3
}
ratio = 2f / 3
lastItem = info
Resizer(this, movable = false)
}
/**
* Launched App using
* -------------------------------------------
* basicListBehavior(),
* basicLabelBehavior(),
* basicSplitPanelBehavior(showDivider = true),
* nativeScrollPanelBehavior()
*/
//sampleEnd
}Requires a SplitPanelBehavior for rendering. BasicTheme provides one.
TabbedPanel
Shows one of several items at a time, switched between with tabs. It takes two ItemVisualizers — one turns each item into the little tab you click, the other turns it into the main content shown underneath. With basicTabbedPanelBehavior, that main content gets scaled to fit the panel; wrap it in a ScrollPanelVisualizer instead if your tabs are meant to hold arbitrary Views that shouldn't be squeezed to fit.
This is also the control used as the running example on the Theming page for just how much a Behavior can take on — worth a look if you're customizing one heavily.
- Demo
- Usage
package tabbedpanel
import io.nacular.doodle.controls.ScrollPanelVisualizer
import io.nacular.doodle.controls.StringVisualizer
import io.nacular.doodle.controls.invoke
import io.nacular.doodle.controls.itemVisualizer
import io.nacular.doodle.controls.panels.TabbedPanel
import io.nacular.doodle.core.view
import io.nacular.doodle.geometry.Size
import io.nacular.doodle.utils.Resizer
fun example() {
//sampleStart
val object1 = view {}
val object2 = view {}
val object3 = view {}
val object4 = view {}
val mapping = mapOf(
object1 to "Circle",
object2 to "Second Tab",
object3 to "Cool Photo",
object4 to "Tab 4"
)
val panel = TabbedPanel(
visualizer = ScrollPanelVisualizer(), // Each object is displayed within a ScrollPanel
tabVisualizer = itemVisualizer { item,_,_ -> StringVisualizer(mapping[item] ?: "Unknown") }, // Each tab shows a hardcoded string from mapping
object1,
object2,
object3,
object4
).apply {
suggestSize(Size(500, 300))
Resizer(this, movable = false)
}
//sampleEnd
}This control requires a TabbedPanelBehavior for rendering. This demo uses the basicTabbedPanelBehavior module which installs BasicTabbedPanelBehavior
Accordion
Like TabbedPanel in spirit — several sections, one focus at a time, and the same two-visualizer split (one for the section header, one for its content) — but stacked vertically with expand/collapse instead of side-by-side tabs. Reach for this over TabbedPanel when your sections read naturally as a list rather than parallel pages.
- Demo
- Usage
package accordion
import io.nacular.doodle.controls.Accordion
import io.nacular.doodle.controls.MultiExpansionModel
import io.nacular.doodle.controls.StringVisualizer
import io.nacular.doodle.controls.ViewVisualizer
import io.nacular.doodle.controls.invoke
import io.nacular.doodle.core.View.Companion.fixed
import io.nacular.doodle.core.view
import io.nacular.doodle.drawing.Color.Companion.Black
import io.nacular.doodle.drawing.Color.Companion.Green
import io.nacular.doodle.drawing.Color.Companion.Orange
import io.nacular.doodle.drawing.Color.Companion.Red
import io.nacular.doodle.drawing.paint
import io.nacular.doodle.geometry.Size
import io.nacular.doodle.utils.Resizer
fun example() {
//sampleStart
val sections = listOf(
view { preferredSize = fixed(Size(20, 200)); render = { rect(bounds.atOrigin, Black.paint ) } },
view { preferredSize = fixed(Size(20, 100)); render = { rect(bounds.atOrigin, Red.paint ) } },
view { preferredSize = fixed(Size(20, 175)); render = { rect(bounds.atOrigin, Green.paint ) } },
view { preferredSize = fixed(Size(20, 90)); render = { rect(bounds.atOrigin, Orange.paint) } },
)
val accordion = Accordion(
visualizer = ViewVisualizer, // Each object is displayed directly
sectionVisualizer = StringVisualizer { "Section ${sections.indexOf(it) + 1}" }, // Each section shows a hardcoded string with index
MultiExpansionModel(),
sections[0],
*sections.drop(1).toTypedArray()
).apply {
suggestSize(Size(500, 300))
Resizer(this, movable = false)
}
//sampleEnd
}This control requires a AccordionBehavior for rendering. This demo uses the basicAccordionBehavior module which installs BasicAccordionBehavior
Recap
GridPanelandSplitPanelarrange Views by position — a grid, or a resizable two-way split.TabbedPanelandAccordionboth show one section at a time from a set — pickTabbedPanelfor parallel pages,Accordionfor a vertically stacked list of sections.- Nest
SplitPanelsinside each other for arrangements more complex than a single divide.
Next: on to Media & Pickers for images, files, and specialized selection controls.