layout

package
v0.0.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: Apache-2.0 Imports: 2 Imported by: 0

Documentation

Overview

Package layout divides a region among the things that go in it.

It is geometry and nothing else: it decides where boxes go and hands back the views to draw them in, and it never draws. That is what lets the same rules place a widget, a string, or a hole left deliberately empty — and what keeps the sizing rules testable by asking for numbers rather than by reading a screen.

Measuring

A slot whose size follows from its content says so with Measured and supplies a Measurer. The measurer is asked about the axis being divided, given how much room there is across the other one: a row of text asked how tall it is at a width, a column of labels asked how wide it is at a height. One question, either axis, which is why Measured means the same thing in Rows and in Columns.

The other axis, and the room between

Dividing an axis leaves two questions it cannot ask, and both were being answered by hand above this package before they were answered here. Flow is an axis with a gap between the things it divides. Slot.Cross is where a slot's content sits when it is narrower than the slot — a centred row of buttons, a hint row against the right edge.

This is not the beginning of a flexbox. Deeply nested layout is refused on purpose, and a caller who wants it will outgrow this: what is here is the three things a terminal interface asks for often enough that everyone writes them again.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Columns

func Columns(v grid.View, slots ...Slot) []grid.View

Columns is Rows across, for panes side by side.

func Divide

func Divide(total, across int, slots []Slot) []int

Divide splits total among slots, measuring against across, and returns each slot's size. The sizes always add up to at most total.

It is exported because a caller aligning something to the same grid — a header over a table, a ruler beside a pane — needs the numbers without the views.

func Rows

func Rows(v grid.View, slots ...Slot) []grid.View

Rows divides v into horizontal bands down the region and returns the view for each, in order.

Example
package main

import (
	"fmt"

	"github.com/Tangerg/oolong/core/grid"
	"github.com/Tangerg/oolong/core/layout"
)

func main() {
	// The order of business is measure, then arrange. Nothing is drawn: the caller
	// draws into the views it is handed, which is what lets a slot be left empty.
	views := layout.Rows(grid.NewSurface(20, 10).View(),
		layout.Slot{Size: layout.Fixed(1)},
		layout.Slot{Size: layout.Flex(1)},
		layout.Slot{Size: layout.Fixed(2)},
	)
	for i, v := range views {
		_, h := v.Size()
		fmt.Printf("slot %d: %d rows\n", i, h)
	}

}
Output:
slot 0: 1 rows
slot 1: 7 rows
slot 2: 2 rows

func Wanted added in v0.0.2

func Wanted(across int, slots []Slot) int

Wanted is how much of the divided axis a set of slots asks for altogether, measured against across.

It is what something made of slots answers when it is itself in a measured slot: a column of widgets inside a pane that grows to fit its contents. A flexible slot has nothing to ask for — a share is a share of a total, and there is no total yet — so it counts as its floor.

Types

type Align

type Align uint8

Align is how content sits in a space wider than itself.

const (
	Start Align = iota
	Center
	End
)

Where content sits when it is narrower than its space.

func (Align) Offset

func (a Align) Offset(space, width int) int

Offset is where content of the given width starts inside space columns.

type Anchor

type Anchor uint8

Anchor is where a floating layer sits in the space it floats over.

const (
	// Middle is the centre, which is where a modal belongs: it is the one position
	// that does not imply the thing it covers is still reachable.
	Middle Anchor = iota
	TopLeft
	Top
	TopRight
	Left
	Right
	BottomLeft
	Bottom
	BottomRight
)

Where a layer can sit: the centre, then the eight edges and corners clockwise from the top left. Middle is first because it is the zero value.

type Axis added in v0.0.2

type Axis uint8

Axis is which way a region is divided.

It exists so that something arranging its contents can be told which way round it goes instead of being written twice. Rows and Columns are the two values of it under the names a caller usually wants.

const (
	// Down stacks bands one above another, dividing height.
	Down Axis = iota
	// Across puts panes side by side, dividing width.
	Across
)

func (Axis) Rects added in v0.0.2

func (a Axis) Rects(space Size, slots []Slot) []image.Rectangle

Rects is where each slot goes when a space is divided along the axis, in the space's own coordinates.

It is the geometry on its own, without a view to draw into, because working out where something went and drawing it there happen at different times: a click arrives between two frames and has to be answered against the frame that is on screen. Anything routing input by position asks this and keeps the answer.

The order of business is measure, then arrange: the only order that works when one slot's size depends on its content and another's depends on what is left. Slots that end up with no room still get a rectangle — an empty one — because a caller's code runs every frame, and code that only breaks when it is squeezed to nothing breaks in front of the user.

func (Axis) Views added in v0.0.2

func (a Axis) Views(v grid.View, slots ...Slot) []grid.View

Views divides v along the axis and returns the view for each slot, in order.

Nothing is drawn. The caller draws into the views it is given, which is what lets a slot be left empty, be drawn conditionally, or be measured now and drawn later.

type Cross added in v0.0.2

type Cross struct {
	// Size is how many cells across the other axis the content takes. Zero, and
	// anything larger than the region, is all of it.
	Size int
	// Align is where it sits when it is less than all of it.
	Align Align
}

Cross is how much of the other axis a slot's content takes, and where in the slot it sits when that is less than all of it.

It is the answer to the one question dividing an axis cannot ask: a row of buttons centred in a dialog, a hint row against the right edge, a title over a pane that is wider than the title. Without it a caller has to take the view it was given and narrow it by hand, which is arithmetic every caller writes and one of them gets wrong.

The size is a number rather than a Measurer on purpose. A widget answers about one axis — Rows asks how tall at a width, Columns how wide at a height — and a slot that asked the other way round would be asking most widgets a question they cannot answer.

type Flow added in v0.0.2

type Flow struct {
	Axis Axis
	// Gap is how many cells go between one slot and the next.
	//
	// It is reserved for every join, including the ones beside a slot that ended up
	// with no room. A gap that appeared and disappeared with its neighbour's contents
	// would move every column after it whenever a value happened to be empty, and a
	// table whose columns shift as its rows change is worse than one with a wider
	// margin than it needed.
	Gap int
}

Flow is an axis with room between the things it divides.

The gap is here rather than in Slot because it is one answer for the whole division: a caller says "these, with a column between them" once, instead of padding every slot but the last and getting the last one wrong. Rows and Columns are this with no gap, which is why they are still the ordinary call.

Example
package main

import (
	"fmt"

	"github.com/Tangerg/oolong/core/grid"
	"github.com/Tangerg/oolong/core/layout"
)

func main() {
	// A gap between the panes, said once for the division rather than as padding on
	// every pane but the last — and a hint row centred under them, which is the other
	// thing dividing an axis cannot say.
	rows := layout.Rows(grid.NewSurface(24, 4).View(),
		layout.Slot{Size: layout.Flex(1)},
		layout.Slot{Size: layout.Fixed(1), Cross: layout.Cross{Size: 8, Align: layout.Center}},
	)
	panes := layout.Flow{Axis: layout.Across, Gap: 2}.Views(rows[0],
		layout.Slot{Size: layout.Part(1, 2)},
		layout.Slot{Size: layout.Flex(1)},
		layout.Slot{Size: layout.Flex(1)},
	)
	for _, p := range panes {
		w, h := p.Size()
		fmt.Printf("pane %dx%d\n", w, h)
	}
	w, _ := rows[1].Size()
	fmt.Printf("hints %d wide\n", w)

}
Output:
pane 10x3
pane 5x3
pane 5x3
hints 8 wide

func (Flow) Divide added in v0.0.2

func (f Flow) Divide(total, across int, slots []Slot) []int

Divide splits total among the slots, holding back the gaps between them first.

func (Flow) Rects added in v0.0.2

func (f Flow) Rects(space Size, slots []Slot) []image.Rectangle

Rects is where each slot goes when a space is divided, in the space's own coordinates.

func (Flow) Views added in v0.0.2

func (f Flow) Views(v grid.View, slots ...Slot) []grid.View

Views divides v and returns the view for each slot, in order.

func (Flow) Wanted added in v0.0.2

func (f Flow) Wanted(across int, slots []Slot) int

Wanted is how much of the divided axis the slots ask for altogether, the gaps between them included.

type Inset

type Inset struct{ Top, Right, Bottom, Left int }

Inset is space held clear on each side.

func Symmetric

func Symmetric(vertical, horizontal int) Inset

Symmetric is one inset above and below, another to the left and right — the common case, because a terminal cell is about twice as tall as it is wide and even padding does not look even.

func Uniform

func Uniform(n int) Inset

Uniform is the same inset on every side.

func (Inset) Apply

func (i Inset) Apply(r image.Rectangle) image.Rectangle

Apply is what is left of r after the inset is held clear, and nothing at all when the inset is larger than the region.

The rectangle is built by hand rather than with image.Rect, which puts a backwards rectangle the right way round: an inset that overran its region would come back as a real region somewhere else instead of as no region at all.

func (Inset) Size

func (i Inset) Size() Size

Size is how many columns and rows the inset takes.

type MeasureFunc

type MeasureFunc func(across int) int

MeasureFunc adapts a function to Measurer.

func (MeasureFunc) Measure

func (f MeasureFunc) Measure(across int) int

Measure calls f.

type Measurer

type Measurer interface {
	Measure(across int) int
}

Measurer reports how much of one axis something wants, given how much room it has across the other.

Which axis is which is decided by whoever is asking: Rows divides height and asks for a height at a width, Columns divides width and asks for a width at a height. A type that can only answer for one axis is a type that belongs in only one of them, and saying so is the caller's business rather than this package's.

type Placement

type Placement struct {
	Anchor Anchor
	// Width and Height are the layer's size in cells. Zero means as large as the
	// space allows, less Margin.
	Width, Height int
	// Margin is kept clear between the layer and the edges of the space, so a layer
	// anchored to a corner does not look stuck to it.
	Margin int
}

Placement is where a floating layer goes in the space it floats over.

It is placement and nothing else — no drawing, no dimming, no idea what is going to be put there. Keeping it that way is what lets a hit test a frame later ask exactly the question the frame asked, and get exactly the same answer.

The layer is clamped to the space rather than allowed to hang off the edge. A dialog whose buttons are past the right margin is a dialog nobody can answer.

func (Placement) In

func (p Placement) In(space Size) image.Rectangle

In is where the layer goes inside a space of the given size, in that space's own coordinates.

type Size

type Size struct{ W, H int }

Size is a width and a height in cells.

type Sizing

type Sizing struct {
	// Fixed is an exact number of rows or columns. It wins over everything else.
	Fixed int
	// Part and Whole are a share of the whole division: Part 1 of Whole 2 is half of
	// it, whatever else is being divided and whatever those others ask for. The whole
	// is what there is to divide, which is the region less the gaps in it.
	//
	// It is not [Flex] and cannot be written as one. A share of what is left changes
	// when anything beside it changes, which is what makes it right for panes that
	// divide the slack and wrong for "this pane is half the screen" — a sentence a
	// caller means literally, and one whose answer must not move when a status bar
	// appears above it.
	Part, Whole int
	// Flex is a share of what is left after the fixed, measured and part slots have
	// taken theirs. Two slots with flex 1 and 2 split the remainder one third to two
	// thirds.
	Flex int
	// Measured asks the slot's [Measurer] how much it wants.
	Measured bool
	// Min is a floor on a flex or measured slot, so a pane cannot be squeezed into a
	// size where it shows nothing useful. It is honoured while there is room for it:
	// several floors can add up to more than the space there is.
	Min int
	// Max caps a measured slot, so content that grew without bound does not take the
	// whole region.
	Max int
}

Sizing says how much of an axis a slot wants.

func Fixed

func Fixed(n int) Sizing

Fixed is a slot of an exact size.

func Flex

func Flex(share int) Sizing

Flex is a slot taking a share of what is left.

func Measured

func Measured(minimum, maximum int) Sizing

Measured is a slot as big as its Measurer asks to be, within bounds. A zero maximum means no cap.

Example
package main

import (
	"fmt"

	"github.com/Tangerg/oolong/core/grid"
	"github.com/Tangerg/oolong/core/layout"
)

func main() {
	// A measured slot is asked about the axis being divided, given the room across
	// the other one — so Measured means the same thing in a row and in a column.
	wide := layout.MeasureFunc(func(across int) int { return across / 4 })
	rows := layout.Rows(grid.NewSurface(20, 10).View(),
		layout.Slot{Size: layout.Measured(0, 0), Of: wide},
		layout.Slot{Size: layout.Flex(1)},
	)
	cols := layout.Columns(grid.NewSurface(20, 8).View(),
		layout.Slot{Size: layout.Measured(0, 0), Of: wide},
		layout.Slot{Size: layout.Flex(1)},
	)
	_, h := rows[0].Size()
	w, _ := cols[0].Size()
	fmt.Printf("measured against a width of 20: %d rows\n", h)
	fmt.Printf("measured against a height of 8: %d columns\n", w)

}
Output:
measured against a width of 20: 5 rows
measured against a height of 8: 2 columns

func Part added in v0.0.2

func Part(part, whole int) Sizing

Part is a slot taking a fraction of the whole division: Part(1, 2) is half of it, whatever else is there. A whole of zero asks for nothing, which is what makes the zero Sizing mean what it always did.

type Slot

type Slot struct {
	Size Sizing
	// Of is asked how much of the divided axis this slot wants, and is only
	// consulted when Size says the slot is measured. A measured slot with nothing to
	// ask gets its floor, which is zero unless one was set.
	Of Measurer
	// Cross is where the slot's content sits across the other axis. The zero value
	// fills it, which is what a band across a pane means and what every slot did
	// before there was a way to say otherwise.
	Cross Cross
}

Slot is one division of a region: how much room it gets, and what to ask when that follows from its content.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL