Where's my view?
Coming right off Theming, let's look at what happens when that step gets skipped — the single most common "why isn't anything showing up" moment in Doodle, and how to spot it fast the next time it happens.
Here's a baseline that works exactly as you'd expect: a single top-level View that fills the Display and draws a centered circle.
package simplecircle
import io.nacular.doodle.application.Application
import io.nacular.doodle.application.application
import io.nacular.doodle.core.Display
import io.nacular.doodle.core.center
import io.nacular.doodle.core.height
import io.nacular.doodle.core.view
import io.nacular.doodle.core.width
import io.nacular.doodle.drawing.Color.Companion.Red
import io.nacular.doodle.drawing.Color.Companion.White
import io.nacular.doodle.drawing.paint
import io.nacular.doodle.geometry.Circle
import org.kodein.di.instance
import kotlin.math.min
//sampleStart
class SimpleCircle(display: Display): Application {
init {
display += view {
suggestSize(display.size)
render = {
circle(
Circle(
center = display.center,
radius = min(display.width, display.height) / 2 - 10
), fill = Red.paint)
}
}
display.fill(White.paint)
}
override fun shutdown() {}
}
fun main() {
application {
SimpleCircle(display = instance())
}
}
//sampleEnd
Where is my Button?
Now let's try the same instinct with a PushButton. This feels like it should work exactly the same way — but it doesn't:
package invisiblebutton
import io.nacular.doodle.application.Application
import io.nacular.doodle.application.application
import io.nacular.doodle.controls.buttons.PushButton
import io.nacular.doodle.core.Display
import io.nacular.doodle.drawing.Color.Companion.White
import io.nacular.doodle.drawing.paint
import io.nacular.doodle.geometry.Size
import org.kodein.di.instance
//sampleStart
class InvisibleButton(display: Display): Application {
init {
// NOTE: This does not render because the button has no
// Behavior installed
display += PushButton().apply { suggestSize(Size(40, 20)) }
display.fill(White.paint)
}
override fun shutdown() {}
}
fun main() {
application {
InvisibleButton(display = instance())
}
}
//sampleEnd
The above examples does not render anything because PushButton delegates all rendering to its behavior, and this button hasn't been given one.
If this looks familiar, it should: it's the exact thing you just read about on Theming — PushButton, like most Views in the Controls library, doesn't draw itself at all. It hands that job off to a Behavior, and with none installed, there's simply nothing telling it how to draw. Two ways to fix that, and they're worth knowing both.
Fix it directly with a Behavior
The straightforward option: hand the button a Behavior yourself.
package buttonwithbehavior
import io.nacular.doodle.application.Application
import io.nacular.doodle.application.Modules.Companion.PointerModule
import io.nacular.doodle.application.application
import io.nacular.doodle.controls.buttons.PushButton
import io.nacular.doodle.controls.theme.simpleButtonRenderer
import io.nacular.doodle.core.Display
import io.nacular.doodle.drawing.Color.Companion.Lightgray
import io.nacular.doodle.drawing.Color.Companion.White
import io.nacular.doodle.drawing.darker
import io.nacular.doodle.drawing.paint
import io.nacular.doodle.geometry.Size
import io.nacular.doodle.layout.constraints.center
import io.nacular.doodle.layout.constraints.constrain
import org.kodein.di.instance
//sampleStart
class ButtonWithBehavior(display: Display): Application {
init {
display += PushButton("Hi").apply {
suggestSize(Size(80, 40))
// Assign a Behavior to the button to handle rendering
behavior = simpleButtonRenderer { button, canvas ->
var color = Lightgray
if (button.model.pressed ) color = color.darker(0.25f)
if (button.model.pointerOver) color = color.darker(0.25f)
canvas.rect(button.bounds.atOrigin, radius = 10.0, fill = color.paint)
}
// acceptsThemes = false // prevents any theme from overriding behavior
}
display.layout = constrain(display.first(), center)
display.fill(White.paint)
}
override fun shutdown() {}
}
fun main() {
// PointerModule required to support mouse/touch
application(modules = listOf(PointerModule)) {
ButtonWithBehavior(display = instance())
}
}
//sampleEnd
Or let a Theme handle it
The other option, and generally the better one once you have more than a button or two: a Theme assigns Behaviors to your whole app at once, rather than one View at a time. Here, registering the nativeButtonBehavior (Web, Desktop) module makes a Theme available that gives every button in the app a native-looking Behavior by default — injected alongside ThemeManager, which is what actually selects it:
package buttonwiththeme
import io.nacular.doodle.application.Application
import io.nacular.doodle.application.Modules.Companion.PointerModule
import io.nacular.doodle.application.application
import io.nacular.doodle.controls.buttons.PushButton
import io.nacular.doodle.core.Display
import io.nacular.doodle.docs.apps.ButtonWithTheme
import io.nacular.doodle.drawing.Color.Companion.White
import io.nacular.doodle.drawing.paint
import io.nacular.doodle.geometry.Size
import io.nacular.doodle.layout.constraints.center
import io.nacular.doodle.layout.constraints.constrain
import io.nacular.doodle.theme.Theme
import io.nacular.doodle.theme.ThemeManager
import io.nacular.doodle.theme.native.NativeTheme.Companion.nativeButtonBehavior
import org.kodein.di.instance
//sampleStart
class ButtonWithTheme(display: Display, themeManager: ThemeManager, theme: Theme): Application {
init {
// install theme that provides a button behavior
themeManager.selected = theme
display += PushButton("Hi").apply { suggestSize(Size(80, 40)) }
display.layout = constrain(display.first(), center)
display.fill(White.paint)
}
override fun shutdown() {}
}
fun main() {
application(modules = listOf(PointerModule, nativeButtonBehavior())) {
ButtonWithTheme(
display = instance(),
themeManager = instance(),
theme = instance()
)
}
}
//sampleEnd
Same result as the manual fix — a button that delegates all its rendering to a Behavior — just applied consistently across your whole app instead of one View at a time.
Recap
- If a control from the
Controlslibrary renders as nothing, check for a missingBehaviorfirst — it's the most common cause by far. - Assign one directly (
button.behavior = ...) for a quick, local fix, or install aThemeto cover your whole app consistently. acceptsThemes = falselocks in a manually-assignedBehavior, so aThemeinstalled later can't quietly override it.
Next: with that gotcha out of the way, on to the Catalog itself.