Positioning Views
Back on Canvas, you saw how a View draws itself once it already has a size and position. Fair question, then: where do that size and position actually come from? That's what this page — the first of two in the Layout section — sets out to answer. The second, Constraints, digs into Doodle's most-used Layout in depth.
A negotiation, not a command
You never set a View's bounds directly — there's no view.bounds = ... waiting to work. Instead, every View's size and position emerge from a three-step negotiation with its parent:
- The parent proposes a range. Through its
Layout, the parent decides the minimum and maximum size the View is allowed to have. - The View picks a size within that range, via its
preferredSize. - The parent positions the View at whatever size was picked, as part of arranging all of its children.
That's why every bounds-related method on View is named suggest* (suggestPosition, suggestSize, suggestBounds) rather than set* — you're a participant in this negotiation, not the sole decision-maker. So let's meet the other participant: the parent's Layout.
Positioning a View with no Layout
Here's the easy case first. If a View has no parent Layout at all, there's nothing to negotiate with — whatever you suggest simply becomes the answer:
import io.nacular.doodle.core.Display
import io.nacular.doodle.core.view
fun positioning(display: Display) {
//sampleStart
val view = view {}
val panel = view {}
view.suggestX (10.0 ) // suggest move to [10, 0]
view.suggestPosition(13.0, -2.0) // suggest reposition to [13,-2]
display += panel // panel's position is 0,0, since nothing suggested otherwise
//sampleEnd
}
This is also the trick behind pointer-driven resizing — Resizer simply watches for pointer events and calls suggestBounds on your behalf:
import io.nacular.doodle.core.View
import io.nacular.doodle.geometry.Rectangle
import io.nacular.doodle.utils.Resizer
fun resizer(view: View) {
//sampleStart
view.apply {
suggestBounds(Rectangle(100, 100))
Resizer(this) // monitors the View and manages resize/move
}
//sampleEnd
}
The Layout interface
Once a View has more than one child, positioning each one by hand gets tedious fast. A Layout automates exactly the negotiation we just walked through: it watches a View's children and rearranges them automatically whenever something shifts: i.e. the parent's own size, or any child's bounds.
import io.nacular.doodle.core.container
import io.nacular.doodle.layout.HorizontalFlowLayout
fun horizontalLayout() {
//sampleStart
container {
layout = HorizontalFlowLayout() // wraps children left-to-right within its bounds
}
//sampleEnd
}
Doodle ships several handy Layouts, including a powerful constraint-based one — substantial enough to earn its own page. You're also free to write your own, either by implementing Layout directly or with the simpleLayout shorthand:
import io.nacular.doodle.core.Layout
import io.nacular.doodle.core.Layout.Companion.simpleLayout
import io.nacular.doodle.core.Positionable
import io.nacular.doodle.geometry.Rectangle
import io.nacular.doodle.geometry.Size
import io.nacular.doodle.layout.Insets
fun example() {
//sampleStart
class CustomLayout: Layout {
override fun layout(views: Sequence<Positionable>, min: Size, current: Size, max: Size, insets: Insets): Size {
views.filter { it.visible }.forEach { child ->
child.bounds = Rectangle(/*...*/)
}
return current
}
}
// DSL for basic layout
simpleLayout { views, min, current, max, insets ->
views.filter { it.visible }.forEach { child ->
child.bounds = Rectangle(/*...*/)
}
current
}
//sampleEnd
}
How a View picks its own size
Step 2 of the negotiation — the View choosing a size within its parent's range — comes down to two properties:
preferredSizeis what aViewactually uses to decide. By default, it defers to the View's ownlayout(if it has one) or its last size suggestion (if it doesn't). You can override it with your own lambda if you need different behavior.sizeAuditoris called any time a View's size is about to change, with the current size, the proposed new size, and the min/max range to stay inside. Whatever it returns still gets clamped into that range — it's a hook for adjusting a proposed size, not bypassing the range entirely. This is what makes fixed-aspect-ratio Views straightforward; Doodle even ships an auditor for exactly that.
View transforms
A transform changes how a View is drawn — even its apparent size — without ever touching what Layout thinks its size is. A View's bounds stay exactly as Layout set them; only its boundingBox (used for rendering and hit-detection) reflects the transform. That split is deliberate — it's what lets you rotate or skew a View freely without dragging its parent's Layout into a fight over space.
Changing a View's transform never triggers a re-layout. And boundingBox == bounds exactly when the transform is Identity — i.e., no transform at all.
This page only covers how transforms interact with Layout — see Transforms for the full picture, including 3D.
Recap
- A View's bounds come from a negotiation: the parent proposes a min/max range, the View picks a size within it via
preferredSize, and the parent positions it. That's why every bounds-setter issuggest*, notset*. - With no
Layoutat all, your suggestion just wins outright. - A
Layoutautomates that negotiation across a whole set of children. Write your own withsimpleLayout, or use Doodle's constraint-based one, covered next. - A
transformchanges how a View renders without changing what Layout thinks its size is.
Next: on to Constraints, Doodle's most-used Layout.