Skip to main content

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:

  1. The parent proposes a range. Through its Layout, the parent decides the minimum and maximum size the View is allowed to have.
  2. The View picks a size within that range, via its preferredSize.
  3. 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 }
info

A View's position is relative to its parent, and is exactly where it renders — unless it (or an ancestor) has a transform applied, which is its own topic further down.

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

A View's layout property is protected by default, like children on the Views page. Subclasses that want to expose it — like Container above — do so deliberately, not by accident.

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:

  • preferredSize is what a View actually uses to decide. By default, it defers to the View's own layout (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.
  • sizeAuditor is 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.

info

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 is suggest*, not set*.
  • With no Layout at all, your suggestion just wins outright.
  • A Layout automates that negotiation across a whole set of children. Write your own with simpleLayout, or use Doodle's constraint-based one, covered next.
  • A transform changes how a View renders without changing what Layout thinks its size is.

Next: on to Constraints, Doodle's most-used Layout.