layout

package
v0.0.0-...-64e189b Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package layout provides simple box-model composition on top of the ansi package: padding, borders, and side-by-side or stacked joins. It measures content with ansi.Width, so text already wrapped in ansi.Style codes still aligns correctly.

Package layout composes pre-rendered blocks and measured nodes into terminal layouts.

A Node is a two-pass measure/render protocol: it reports a preferred Size under Constraints (Measure) and is then handed a final Size to fill exactly (Render). Block adapts a pre-rendered string. Row, Column, RowJustify and ColumnJustify size FlexChild values along the main axis with Basis, BasisLen (Pct and Fr), Grow, Shrink, Min and Max, place them on the cross axis with CrossAlign, and space them with Justify. Fill and FillWeight take leftover space; Scroll windows a tall child; BoxNode, GridNode, MinSize, Responsive and Overlay compose nodes; Text is a wrapping text node. Draw renders at the measured size (Loose); DrawTight renders at exactly a given size so Fill children absorb all remaining space. A node that also implements CellNode draws straight into a CellSurface (DrawTo, with cellbuf.Layout adapting a cellbuf.Buffer) without building a string.

Box, Overlay and JoinHorizontalJustify work on pre-rendered strings. The string join helpers that once sat beside them (JoinHorizontal, JoinVertical, FlexRow, Grid, GridFlex) were removed in v1.0; docs/migrating-to-v1.md maps each to its node.

This file adds the two-pass measure/render protocol from decision #10. It is purely additive: Box, Join*, Grid, FlexRow, GridFlex and Overlay keep their string-in/string-out signatures and output. A Node lets a parent ask a child how big it wants to be (Measure, top-down constraints, bottom-up preferred size) before handing it a final Size to fill exactly (Render). Block adapts any pre-rendered string, so existing widgets compose immediately.

Example (GrowTrap)

A plain Grow child that measures taller than the screen pushes its siblings out of view. Compare ExampleFill, which keeps the footer.

package main

import (
	"fmt"
	"strings"

	"github.com/ows4444/tui/layout"
)

// show prints out with trailing spaces trimmed (Go example output is
// compared verbatim, and Render pads every row to the full width).
func show(out string) {
	for _, l := range strings.Split(out, "\n") {
		fmt.Println(strings.TrimRight(l, " "))
	}
}

func main() {
	lines := make([]string, 100)
	for i := range lines {
		lines[i] = fmt.Sprintf("log %02d", i)
	}
	ui := layout.Column(0,
		layout.FlexChild{Node: layout.Block("== header ==")},
		layout.FlexChild{Node: layout.Block(strings.Join(lines, "\n")), Grow: 1},
		layout.FlexChild{Node: layout.Block("== footer ==")},
	)
	show(layout.Draw(ui, layout.Constraints{MinW: 12, MaxW: 12, MinH: 5, MaxH: 5}))
}
Output:
== header ==
log 00
log 01
log 02
log 03
Example (MeasureRender)

A Node tree is sized in two passes: Draw measures the tree under the given constraints, then renders it at the measured size. Here the window is pinned to 24x6, so the body row grows to fill what the header and footer leave, and the sidebar keeps a fixed basis.

package main

import (
	"fmt"
	"strings"

	"github.com/ows4444/tui/layout"
)

// show prints out with trailing spaces trimmed (Go example output is
// compared verbatim, and Render pads every row to the full width).
func show(out string) {
	for _, l := range strings.Split(out, "\n") {
		fmt.Println(strings.TrimRight(l, " "))
	}
}

func main() {
	ui := layout.Column(0,
		layout.FlexChild{Node: layout.BoxNode(layout.NewBox().Border(layout.NormalBorder()), layout.Block("title"))},
		layout.FlexChild{Grow: 1, Node: layout.Row(1,
			layout.FlexChild{Node: layout.Block("nav"), Basis: 5},
			layout.FlexChild{Node: layout.Block("body"), Grow: 1},
		)},
		layout.FlexChild{Node: layout.Block("q: quit")},
	)
	show(layout.Draw(ui, layout.Constraints{MinW: 24, MaxW: 24, MinH: 6, MaxH: 6}))
}
Output:
┌──────────────────────┐
│title                 │
└──────────────────────┘
nav   body

q: quit
Example (WidgetNodeIsNotItsView)

A widget's LayoutNode is not the same picture as its View. A textinput's Width limits its View (a longer value scrolls to fit), but its node ignores Width and measures the whole value: in an unconstrained layout it is as wide as the value, and it takes the width the layout gives it (see Row's Basis).

package main

import (
	"fmt"

	"github.com/ows4444/tui/ansi"
	"github.com/ows4444/tui/layout"
	"github.com/ows4444/tui/textinput"
)

func main() {
	in := textinput.New()
	in.Prompt = "Name: "
	in.Width = 12
	in.SetValue("a value much longer than the width")
	fmt.Println("View width:", ansi.Width(in.View()))
	fmt.Println("node width:", ansi.Width(layout.Draw(in.LayoutNode(), layout.Unconstrained())))
	fixed := layout.Row(0, layout.FlexChild{Node: in.LayoutNode(), Basis: 20})
	fmt.Println("node in a Basis-20 Row:", ansi.Width(layout.Draw(fixed, layout.Unconstrained())))
}
Output:
View width: 18
node width: 41
node in a Basis-20 Row: 20

Index

Examples

Constants

View Source
const Unbounded = 1 << 30

Unbounded, used as a Constraints Max, means "no upper limit on this axis". It is a large finite value rather than a special case so bounds arithmetic never needs a branch.

Variables

This section is empty.

Functions

func Draw

func Draw(root Node, c Constraints) string

Draw runs both passes on root: it measures under c, then renders at the measured size. The result is exactly that size, or "" if either dimension is 0. Within one Draw, Measure is called at most once per node per distinct Constraints for the built-in container nodes' children.

Example (Rectangle)

Draw returns a rectangle: every row is padded to the widest one. Compare a drawn view by its content (trim the trailing spaces), not its raw width.

package main

import (
	"fmt"

	"github.com/ows4444/tui/layout"
)

func main() {
	ui := layout.Column(0, layout.FlexChild{Node: layout.Block("ab")}, layout.FlexChild{Node: layout.Block("abcdef")})
	fmt.Printf("%q\n", layout.Draw(ui, layout.Unconstrained()))
}
Output:
"ab    \nabcdef"

func DrawTight

func DrawTight(root Node, size Size) string

DrawTight is Draw under Tight(size): the root is rendered at exactly size, so Fill children absorb all remaining main-axis space. Draw with Loose renders at the measured size and is unchanged.

func JoinHorizontalJustify

func JoinHorizontalJustify(width int, justify Justify, blocks ...string) string

JoinHorizontalJustify lays out blocks side by side within width, top cross-axis-aligned exactly as joinHorizontal is (see joinHorizontalAlign for that logic), distributing any leftover width along the main axis per justify. If the blocks' combined rendered width is already >= width, no gap is added and the result may exceed width — width is a target to fill, not a hard truncation limit. It is additive to joinHorizontal/ joinHorizontalAlign, which take an explicit literal gap instead of computing one from a target width.

func NamedAt

func NamedAt(root Node, s Size, x, y int) (name string, relX, relY int, ok bool)

NamedAt returns the name of the innermost Named node whose Rect contains the cell (x, y) in the tree under root laid out at size s, and (x, y) relative to that node's top-left corner. ok is false when the cell is outside every Named node, so an app can route a click with hit-testing free of a second region list. Coordinates are as in Rects. When Named nodes overlap without nesting, the later one in Rects order wins.

func Overlay

func Overlay(base, overlay string, x, y int) string

Overlay composites overlay on top of base at column x, row y (both 0-indexed, relative to base's own top-left corner), replacing whatever was there while leaving the rest of base untouched.

This exists because Program.render treats View()'s return value as the entire screen with no notion of layered content — a widget that needs to draw on top of the rest of the view (a modal dialog, a toast notification) has to composite that itself before returning from View, and this is the shared primitive for doing that instead of solving it separately per widget.

x and y are clamped to >= 0. Rows of overlay past base's last line are dropped. A base row shorter than x is padded with spaces before the overlay is spliced in. Base content beyond the overlay's right edge is preserved — Overlay never truncates a base line to the overlay's width, only replaces the columns the overlay actually covers. Keeping every resulting line the same width (e.g. for a rectangular bordered box) is the caller's responsibility: size the overlay to fit within base's existing width before calling this.

func Window

func Window(lines []string, cursor, h int) []string

Window returns h consecutive lines from lines that contain the line at index cursor, centred on it when there is room and clamped to the ends. If lines already fit in h (or h <= 0) it returns lines unchanged; the result never has more than h lines.

func WindowRange

func WindowRange(total, cursor, h int) (start, end int)

WindowRange is Window over indices: the half-open range [start, end) of h consecutive items out of total, containing cursor, centred on it when there is room and clamped to the ends. If total already fits in h (or h <= 0) it is [0, total).

Types

type Align

type Align int

Align is a cross-axis alignment for joinHorizontalAlign and joinVerticalAlign. The zero value, AlignStart, reproduces the behaviour of joinHorizontal (top-aligned) and joinVertical (left-aligned).

const (
	// AlignStart top-aligns blocks in joinHorizontalAlign (blank rows are
	// added below each block's content) and left-aligns lines in
	// joinVerticalAlign (lines are right-padded to the common width). It is
	// the zero value, and matches joinHorizontal/joinVertical.
	AlignStart Align = iota
	// AlignCenter vertically centers each block's rows (joinHorizontalAlign)
	// or horizontally centers each line (joinVerticalAlign) within the
	// available space. When the padding is odd, the extra row or column goes
	// after/on the right, matching AlignStart's placement of extra space.
	AlignCenter
	// AlignEnd bottom-aligns blocks in joinHorizontalAlign (blank rows are
	// added above each block's content) and right-aligns lines in
	// joinVerticalAlign (lines are left-padded to the common width).
	AlignEnd
)

type Border

type Border = basetypes.Border

Border is the set of characters drawn around a Box.

func ASCIIBorder

func ASCIIBorder() Border

ASCIIBorder uses only 7-bit ASCII, for terminals or fonts without Unicode box-drawing character support.

func DoubleBorder

func DoubleBorder() Border

DoubleBorder draws double lines.

func NormalBorder

func NormalBorder() Border

NormalBorder draws square corners with thin lines.

func RoundedBorder

func RoundedBorder() Border

RoundedBorder draws rounded corners with thin lines.

func ThickBorder

func ThickBorder() Border

ThickBorder draws heavy lines.

type Box

type Box struct {
	// contains filtered or unexported fields
}

Box is an immutable, chainable builder for a padded, optionally bordered block of text — the structural counterpart to ansi.Style.

func NewBox

func NewBox() Box

NewBox returns an empty Box: no padding, no border, sized to its content.

func (Box) Background

func (b Box) Background(c ansi.Color) Box

Background fills the box interior (content, padding and the blank fill up to Width) with c. The border and margin are left alone. Content that resets its own styling keeps the background.

func (Box) Border

func (b Box) Border(border Border) Box

Border sets the border style (e.g. layout.NormalBorder()); the zero value draws no border.

func (Box) BorderColor

func (b Box) BorderColor(c ansi.Color) Box

BorderColor draws the border in c (typically a Theme's BorderColor). Each border segment — the top row, each side character, the bottom row — is its own styled span, so every line of the result is self-contained. Nil, the default, leaves the border uncoloured; a box without a border ignores it. Colour changes no dimensions: widths are measured without escape codes.

func (Box) BorderSides

func (b Box) BorderSides(top, right, bottom, left bool) Box

BorderSides chooses which border sides are drawn; all four are on by default. A corner is drawn with its top or bottom edge only when the vertical side beside it is on; with that side off the edge simply runs the box's width.

func (Box) Height

func (b Box) Height(h int) Box

Height fixes the content height in rows (padding and border are drawn outside it); the zero value sizes the box to its content. Shorter content is padded with blank rows and extra rows are cut, so the box is always exactly h plus padding and border tall.

func (Box) Margin

func (b Box) Margin(top, right, bottom, left int) Box

Margin sets blank space outside the border, per side. It is not coloured by Background. Negative values count as 0.

func (Box) Padding

func (b Box) Padding(top, right, bottom, left int) Box

Padding sets the space between the border (or the box's edge, if there is no border) and the content, per side.

func (Box) PaddingAll

func (b Box) PaddingAll(n int) Box

PaddingAll sets n as the padding on all four sides.

func (Box) Render deprecated

func (b Box) Render(content string) string

Render lays content out inside the box: content lines are padded flush to a common width, then padding rows/columns and an optional border are added around them.

Deprecated: use BoxNode(box, Block(content)) or BoxNode with any Node child.

func (Box) Title

func (b Box) Title(s string, a Align) Box

Title sets text drawn in the top border, placed by a: AlignStart near the left corner, AlignCenter centred, AlignEnd near the right. It is clipped to the border, and ignored when the top side is off or there is no border.

func (Box) Width

func (b Box) Width(w int) Box

Width sets a fixed content width (padding and border are drawn outside it); the zero value sizes the box to its widest content line instead. A content line wider than w is clipped to it (ansi.Truncate), so every row of the box is exactly w plus padding and border wide.

type Break

type Break struct{ MaxW, MaxH int }

Break is one breakpoint of Responsive: its node is used while the available width is at most MaxW and the available height is at most MaxH. A MaxW or MaxH of zero or less admits any width or height respectively.

type CellNode

type CellNode interface {
	Node
	DrawCells(dst CellSurface, r Rect)
}

CellNode is an optional interface for a Node that can draw straight into a cell grid instead of building a string. DrawCells draws the node as Render(Size{r.W, r.H}) would, with its top-left corner at r.X, r.Y of dst, clipped to r and to dst's clip, and leaves dst's clip as it found it. It writes only the cells it has ink for: the cells of r are expected to be blank (DrawTo clears the root's rectangle first), so a CellNode need not paint padding. Row, Column, GridNode, BoxNode, Scroll, OverlayNode, Block, Text and StyledText implement it; a container draws a child that does not implement it by rendering it to a string, so any tree can be drawn.

type CellSurface

type CellSurface interface {
	// Clip returns the rectangle drawing is currently clipped to.
	Clip() Rect
	// SetClip clips later drawing to r, itself clipped to the surface.
	SetClip(r Rect)
	// Put writes s, a single-row string that may carry SGR styling, from
	// column x of row y, clipped to the clip rectangle, and returns the
	// columns written. It never wraps and writes nothing for "".
	Put(x, y int, s string) int
	// Repeat writes n copies of the one-cluster (possibly styled) string
	// cluster side by side from column x of row y, clipped to the clip
	// rectangle. A space clears cells.
	Repeat(x, y, n int, cluster string)
}

CellSurface is the grid a CellNode draws into. It is deliberately small and uses only layout types so that this package does not depend on any cell buffer: package cellbuf adapts a *cellbuf.Buffer with cellbuf.Layout. Coordinates are the surface's own, with (0, 0) at its top-left cell.

type Constraints

type Constraints struct{ MinW, MaxW, MinH, MaxH int }

Constraints bounds the Size a Node may report from Measure. A Max of Unbounded leaves that axis open. Constraints are normalized before use: a negative bound counts as 0 and a Max below its Min is raised to it, so any Constraints value is safe to pass. Note the zero value bounds both axes to 0; use Unconstrained or set MaxW/MaxH (to Unbounded) to leave an axis open.

Example (ZeroValue)

The zero Constraints bounds both axes to 0, so nothing fits: use Unconstrained, Loose or an explicit Constraints with MaxH set.

package main

import (
	"fmt"

	"github.com/ows4444/tui/layout"
)

func main() {
	n := layout.Block("hello")
	fmt.Printf("zero value: %q\n", layout.Draw(n, layout.Constraints{}))
	fmt.Printf("unconstrained: %q\n", layout.Draw(n, layout.Unconstrained()))
}
Output:
zero value: ""
unconstrained: "hello"

func Loose

func Loose(max Size) Constraints

Loose returns constraints allowing anything from 0x0 up to max.

func Tight

func Tight(size Size) Constraints

Tight returns constraints that allow exactly size on both axes.

func Unconstrained

func Unconstrained() Constraints

Unconstrained returns constraints with no bounds at all.

func (Constraints) Constrain

func (c Constraints) Constrain(s Size) Size

Constrain clamps s into the constraints' range on both axes.

type CrossAlign

type CrossAlign int

CrossAlign is a per-child cross-axis placement for Row and Column.

Example

A Row stretches every child to the row's full height, so a short box beside a tall one grows to match. CrossStart keeps its own height.

package main

import (
	"fmt"
	"strings"

	"github.com/ows4444/tui/layout"
)

// show prints out with trailing spaces trimmed (Go example output is
// compared verbatim, and Render pads every row to the full width).
func show(out string) {
	for _, l := range strings.Split(out, "\n") {
		fmt.Println(strings.TrimRight(l, " "))
	}
}

func main() {
	tall := layout.Block("a\nb\nc\nd\ne")
	short := layout.Block("x")
	stretched := layout.Row(1, layout.FlexChild{Node: tall}, layout.FlexChild{Node: layout.BoxNode(layout.NewBox().Border(layout.NormalBorder()), short)})
	kept := layout.Row(1, layout.FlexChild{Node: tall}, layout.FlexChild{Node: layout.BoxNode(layout.NewBox().Border(layout.NormalBorder()), short), CrossAlign: layout.CrossStart})
	show(layout.Draw(stretched, layout.Unconstrained()))
	fmt.Println("--")
	show(layout.Draw(kept, layout.Unconstrained()))
}
Output:
a ┌─┐
b │x│
c │ │
d │ │
e └─┘
--
a ┌─┐
b │x│
c └─┘
d
e
const (
	// CrossStretch renders the child at the container's full cross size.
	// It is the zero value and the behaviour Row and Column always had.
	CrossStretch CrossAlign = iota
	// CrossStart renders the child at its own measured cross size, flush
	// to the top of a Row or the left of a Column.
	CrossStart
	// CrossCenter is CrossStart centred on the cross axis; an odd
	// remainder goes after the child.
	CrossCenter
	// CrossEnd is CrossStart flush to the bottom of a Row or the right of
	// a Column.
	CrossEnd
)

type FlexChild

type FlexChild struct {
	Node Node
	// Basis is the preferred main-axis size in cells; 0 means "the size
	// the child measures to". A scrollable child (a viewport, a long
	// table) measures to its full content length, so to make one simply
	// fill the space left over, use Fill (Basis: 1 with Grow: 1); otherwise
	// a growing child that measures larger than the screen pushes its
	// siblings out of view.
	Basis int
	// BasisLen, when set (see Pct and Fr), takes precedence over Basis. Pct
	// sizes the child as a percentage of the main-axis space left after
	// gaps; Fr gives it no base size and a Grow share of n, so fr children
	// split whatever the fixed and percentage children leave. The zero
	// value is unset.
	BasisLen Length
	// Grow, when > 0, shares positive free space by weight.
	Grow int
	// Shrink, when > 0, absorbs overflow, weighted by the child's basis.
	Shrink int
	// Min is the smallest main-axis size the child may be given.
	Min int
	// Max is the largest main-axis size; 0 means unbounded.
	Max int
	// CrossAlign places the child on the container's cross axis (vertical
	// in a Row, horizontal in a Column). The zero value, CrossStretch,
	// gives the child the full cross size.
	CrossAlign CrossAlign
}

FlexChild is one child of a Row or Column, sized along the container's main axis (width for Row, height for Column). The zero value is a content-sized child that neither grows nor shrinks.

func Fill

func Fill(n Node) FlexChild

Fill returns a FlexChild that takes a share of whatever main-axis space is left after its siblings are sized, however large or small the node itself measures. It is Basis: 1 with Grow: 1 (and Shrink: 1, so it also gives space back when the container is too small), which is the idiom scrollable content needs: a viewport or a long table measures to its full length, and a growing child that measures larger than the screen would otherwise push its siblings out of view. Several Fill children split the leftover equally; use FillWeight for unequal shares.

Example

Fill makes a panel take whatever space is left, even when its content is far longer than the screen: the header and footer stay visible and the long log fills the middle.

package main

import (
	"fmt"
	"strings"

	"github.com/ows4444/tui/layout"
)

// show prints out with trailing spaces trimmed (Go example output is
// compared verbatim, and Render pads every row to the full width).
func show(out string) {
	for _, l := range strings.Split(out, "\n") {
		fmt.Println(strings.TrimRight(l, " "))
	}
}

func main() {
	lines := make([]string, 100)
	for i := range lines {
		lines[i] = fmt.Sprintf("log %02d", i)
	}
	ui := layout.Column(0,
		layout.FlexChild{Node: layout.Block("== header ==")},
		layout.Fill(layout.Block(strings.Join(lines, "\n"))),
		layout.FlexChild{Node: layout.Block("== footer ==")},
	)
	show(layout.Draw(ui, layout.Constraints{MinW: 12, MaxW: 12, MinH: 5, MaxH: 5}))
}
Output:
== header ==
log 00
log 01
log 02
== footer ==

func FillWeight

func FillWeight(n Node, weight int) FlexChild

FillWeight is Fill with a share weight: among Fill children, leftover space is split in proportion to weight. A weight below 1 counts as 1.

type Justify

type Justify int

Justify is a main-axis (horizontal) space distribution for JoinHorizontalJustify — CSS flexbox's justify-content, as distinct from Align's cross-axis alignment.

const (
	// JustifyStart packs blocks flush to the left with no gap between
	// them, matching joinHorizontal's own layout. It is the zero value.
	JustifyStart Justify = iota
	// JustifyEnd packs blocks flush to the right: all leftover width
	// becomes a single leading gap.
	JustifyEnd
	// JustifyCenter centers the blocks as a group, splitting leftover
	// width before and after them; an odd remainder goes to the leading
	// gap, matching Align's own placement-of-the-extra-unit convention.
	JustifyCenter
	// JustifySpaceBetween distributes leftover width as equal gaps
	// strictly between consecutive blocks, with no leading or trailing
	// gap. With fewer than two blocks there is no "between" to distribute
	// into, so it behaves like JustifyStart.
	JustifySpaceBetween
	// JustifySpaceAround gives each block an equal half-gap on both
	// sides, so the gap between two blocks (adjoining half-gaps) is
	// twice the width of the leading and trailing gaps.
	JustifySpaceAround
	// JustifySpaceEvenly distributes leftover width as equal gaps
	// before the first block, between every pair, and after the last.
	JustifySpaceEvenly
)

type Length

type Length struct {
	// contains filtered or unexported fields
}

Length is a flexible main-axis size for FlexChild.BasisLen. The zero value is unset. Build one with Pct or Fr.

func Fr

func Fr(n int) Length

Fr is a Length that shares the space left after fixed and percentage children in proportion to n (a value below 1 counts as 1).

func Pct

func Pct(n int) Length

Pct is a Length of n percent of the container's main-axis space after gaps.

type Node

type Node interface {
	Measure(c Constraints) Size
	Render(s Size) string
}

Node is a layout participant. Measure returns the size the node prefers under c (always within c). Render returns a string of exactly s.W columns by s.H rows (ansi.Width-measured); s is the final size the parent allotted, which need not equal what Measure returned. Measure must be side-effect free: parents may call it several times before Render.

func Absolute

func Absolute(x, y int, n Node) Node

Absolute returns n tagged to be placed with its top-left corner at column x, row y of the enclosing Stack (negative values count as 0) instead of at its origin. Outside a Stack it is n. Absolute composes with Layer in either order.

func Block

func Block(s string) Node

Block adapts a pre-rendered (possibly multi-line, possibly styled) string to a Node. Measure reports the string's natural size (widest line by display width, by line count; "" is 0x0). Render pads or clips it to the allotted size: lines are truncated by display width without splitting a rune or an escape sequence, and short lines and missing rows are filled with spaces.

func BoxNode

func BoxNode(box Box, child Node) Node

BoxNode wraps child in box's padding and border as a Node. Measure adds the box's chrome to the child's size; Render gives the child what is left of the allotted Size after the chrome and draws the box around it.

func Column

func Column(gap int, children ...FlexChild) Node

Column lays children out top to bottom, separated by gap blank rows. Children are stretched to the column's full width. Sizing and clipping follow Row, with height as the main axis.

Example (CrossStart)

A Column stretches every child to its own width, so one long line widens the bordered box above it. CrossStart keeps the box at its own width.

package main

import (
	"fmt"
	"strings"

	"github.com/ows4444/tui/layout"
)

// show prints out with trailing spaces trimmed (Go example output is
// compared verbatim, and Render pads every row to the full width).
func show(out string) {
	for _, l := range strings.Split(out, "\n") {
		fmt.Println(strings.TrimRight(l, " "))
	}
}

func main() {
	box := layout.BoxNode(layout.NewBox().Border(layout.NormalBorder()), layout.Block("hi"))
	help := layout.Block("a much longer help line")

	show(layout.Draw(layout.Column(0,
		layout.FlexChild{Node: box},
		layout.FlexChild{Node: help},
	), layout.Unconstrained()))
	fmt.Println()
	show(layout.Draw(layout.Column(0,
		layout.FlexChild{Node: box, CrossAlign: layout.CrossStart},
		layout.FlexChild{Node: help},
	), layout.Unconstrained()))
}
Output:
┌─────────────────────┐
│hi                   │
└─────────────────────┘
a much longer help line

┌──┐
│hi│
└──┘
a much longer help line
Example (Gap)

A Column's gap inserts the blank row between sections. Do not use Block("") as a spacer: it is 0x0, not a blank row.

package main

import (
	"fmt"
	"strings"

	"github.com/ows4444/tui/layout"
)

// show prints out with trailing spaces trimmed (Go example output is
// compared verbatim, and Render pads every row to the full width).
func show(out string) {
	for _, l := range strings.Split(out, "\n") {
		fmt.Println(strings.TrimRight(l, " "))
	}
}

func main() {
	ui := layout.Column(1,
		layout.FlexChild{Node: layout.Block("top")},
		layout.FlexChild{Node: layout.Block("bottom")},
	)
	show(layout.Draw(ui, layout.Unconstrained()))
}
Output:
top

bottom
Example (Window)

A scrolling child measures to its whole content, so on its own it would make the view as tall as the content. A Basis on the child is its window height.

package main

import (
	"fmt"
	"strings"

	"github.com/ows4444/tui/layout"
)

// show prints out with trailing spaces trimmed (Go example output is
// compared verbatim, and Render pads every row to the full width).
func show(out string) {
	for _, l := range strings.Split(out, "\n") {
		fmt.Println(strings.TrimRight(l, " "))
	}
}

func main() {
	lines := make([]string, 100)
	for i := range lines {
		lines[i] = fmt.Sprintf("log %02d", i)
	}
	ui := layout.Column(0,
		layout.FlexChild{Node: layout.Block("== header ==")},
		layout.FlexChild{Node: layout.Block(strings.Join(lines, "\n")), Basis: 3},
		layout.FlexChild{Node: layout.Block("== footer ==")},
	)
	show(layout.Draw(ui, layout.Unconstrained()))
}
Output:
== header ==
log 00
log 01
log 02
== footer ==

func ColumnJustify

func ColumnJustify(gap int, justify Justify, children ...FlexChild) Node

ColumnJustify is Column with main-axis (vertical) Justify, as RowJustify is to Row.

func Fixed

func Fixed(n Node, size Size) Node

Fixed returns n with an exact width, height or both. A zero (or negative) W or H leaves that axis to n, so Fixed(n, Size{W: 40}) fixes only the width and Fixed(n, Size{}) is n itself.

Measure reports the fixed value on each fixed axis and n's own size on each free one, then applies the parent's constraints like every node. Render(s) draws n at the fixed size on each fixed axis and at s on each free one, anchored at the top-left, then clips or pads the result to exactly s: a Fixed larger than its space is clipped, a smaller one is padded with blanks. Rects reports n at its real position, so Named nodes below a Fixed keep their rectangles.

Example

Fixed gives a node an exact width, height or both, whatever it contains. A zero axis is left to the node.

package main

import (
	"fmt"
	"strings"

	"github.com/ows4444/tui/layout"
)

// show prints out with trailing spaces trimmed (Go example output is
// compared verbatim, and Render pads every row to the full width).
func show(out string) {
	for _, l := range strings.Split(out, "\n") {
		fmt.Println(strings.TrimRight(l, " "))
	}
}

func main() {
	box := layout.BoxNode(layout.NewBox().Border(layout.NormalBorder()), layout.Block("hi"))
	fixed := layout.Fixed(box, layout.Size{W: 12})
	show(layout.Draw(fixed, layout.Unconstrained()))
}
Output:
┌──────────┐
│hi        │
└──────────┘

func GridNode

func GridNode(tracks []Track, gap int, cells ...Node) Node

GridNode arranges cells into len(tracks) columns, filling row by row; a short final row leaves its missing cells blank. gap cells separate columns and gap blank rows separate rows; use GridNodeGaps to give the two axes different gaps. Column widths come from the same solver as Row, so every row lines up; each row is as tall as its tallest cell, measured at that column's final width (so a cell can adapt its height to the width it is given), and every cell is stretched to its slot. Rendered at a Size it produces exactly that many columns and rows, padding spare space and clipping overflow like Row and Column. It replaces the string helpers Grid and GridFlex, removed in v1.0.

Example

GridNode aligns columns across rows and lets one column absorb the leftover width.

package main

import (
	"fmt"
	"strings"

	"github.com/ows4444/tui/layout"
)

// show prints out with trailing spaces trimmed (Go example output is
// compared verbatim, and Render pads every row to the full width).
func show(out string) {
	for _, l := range strings.Split(out, "\n") {
		fmt.Println(strings.TrimRight(l, " "))
	}
}

func main() {
	g := layout.GridNode(
		[]layout.Track{{}, {Grow: 1}, {Size: 3}},
		1,
		layout.Block("id"), layout.Block("name"), layout.Block("ok"),
		layout.Block("7"), layout.Block("widget"), layout.Block("no"),
	)
	// MaxH must be set: a zero Max means "at most 0".
	show(layout.Draw(g, layout.Constraints{MinW: 20, MaxW: 20, MaxH: layout.Unbounded}))
}
Output:
id name          ok

7  widget        no

func GridNodeGaps

func GridNodeGaps(tracks []Track, colGap, rowGap int, cells ...Node) Node

GridNodeGaps is GridNode with separate gaps: colGap blank columns between columns and rowGap blank rows between rows. GridNodeGaps(t, 1, 0, cells...) is a table with one space between its columns and no blank line between its rows, which GridNode's single gap cannot express. A negative gap counts as zero. GridNode(tracks, g, cells...) is GridNodeGaps(tracks, g, g, cells...).

Example

GridNodeGaps gives a grid different gaps between columns and between rows. GridNode's single gap would put a blank row between every pair of rows here.

package main

import (
	"fmt"
	"strings"

	"github.com/ows4444/tui/layout"
)

// show prints out with trailing spaces trimmed (Go example output is
// compared verbatim, and Render pads every row to the full width).
func show(out string) {
	for _, l := range strings.Split(out, "\n") {
		fmt.Println(strings.TrimRight(l, " "))
	}
}

func main() {
	g := layout.GridNodeGaps(
		[]layout.Track{{}, {}},
		2, 0, // two blank columns between columns, no blank rows between rows
		layout.Block("api"), layout.Block("up"),
		layout.Block("worker"), layout.Block("down"),
		layout.Block("db"), layout.Block("up"),
	)
	show(layout.Draw(g, layout.Unconstrained()))
}
Output:
api     up
worker  down
db      up

func Layer

func Layer(z int, n Node) Node

Layer returns n tagged with stacking order z for a Stack: of two overlapping children, the one with the higher z is drawn on top. Outside a Stack it is n. Layer composes with Absolute in either order.

func MinMax

func MinMax(n Node, min, max Size) Node

MinMax returns n with its size bounded rather than fixed. A zero (or negative) bound leaves that side open, so MinMax(n, Size{}, Size{}) is n itself. If a max is below its min on an axis, the min wins.

Measure clamps n's own size into [min, max] on each axis, then applies the parent's constraints like every node. Render(s) draws n at s clamped into the same bounds, anchored at the top-left, then clips or pads the result to exactly s. Rects reports n at its real position, so Named nodes below a MinMax keep their rectangles.

func MinSize

func MinSize(n Node, min Size, fallback Node) Node

MinSize returns a Node that renders n only when it has at least min to work with, and fallback otherwise — a "terminal too small" screen instead of a clipped frame. A min axis of zero (or less) is never too small. A nil fallback renders blank space of the allotted size.

Render(s) draws fallback when s.W < min.W or s.H < min.H. Measure follows the same rule against the parent's maximum: when c.MaxW or c.MaxH cannot hold min, it measures fallback, so the two passes agree. Rects reports whichever of the two is showing.

func Named

func Named(name string, n Node) Node

Named returns n with a label that Rects reports in Placed.Name, so an app can find the region a particular widget occupies with RectOf. It changes nothing about layout: Measure and Render pass straight through. An empty name is not searchable.

func OverlayNode

func OverlayNode(base, over Node, x, y int) Node

OverlayNode composites over on top of base at column x, row y (relative to base's top-left; negative values count as 0), replacing what is under it. It is Overlay for nodes: Measure is base's, and over is drawn at its own natural size, cut off at base's allotted Size. Where Overlay works on rendered strings, OverlayNode implements CellNode, so a modal or toast layers over a cell-drawn tree without rendering either to a string.

func Responsive

func Responsive(breaks []Break, nodes ...Node) Node

Responsive picks a node by the available size. breaks[i] pairs with nodes[i]: the first node whose Break admits both the width and the height is used. A node with no Break (nodes longer than breaks) is the fallback for larger sizes; if every break is exceeded and there is no such node, the last node is used. With no nodes it is empty.

Measure uses the node chosen by the constraint's MaxW and MaxH, so the answer is the layout that Render will pick when given that width. Render(s) draws the node chosen for s at exactly s. Rects reports the chosen node at its real position, like Fixed.

Example
package main

import (
	"fmt"

	"github.com/ows4444/tui/layout"
)

func main() {
	ui := layout.Responsive([]layout.Break{{MaxW: 20}}, layout.Block("menu"), layout.Block("home | search | settings"))
	fmt.Println(layout.Draw(ui, layout.Loose(layout.Size{W: 20, H: 3})))
	fmt.Println(layout.Draw(ui, layout.Loose(layout.Size{W: 40, H: 3})))
}
Output:
menu
home | search | settings

func Row

func Row(gap int, children ...FlexChild) Node

Row lays children out left to right, separated by gap columns. Children are stretched to the row's full height. Rendered at a Size it produces exactly that many columns and rows: space left over when no child grows is blank, and overflow that no child can shrink away is clipped by display width (never mid-rune or mid-escape).

func RowJustify

func RowJustify(gap int, justify Justify, children ...FlexChild) Node

RowJustify is Row that also distributes any space left after sizing along the main axis per justify (JustifyStart, the zero value, is plain Row). Space is only left when no child grows, or every growing child hit its Max.

func Scroll

func Scroll(child Node, offset int) Node

Scroll returns a Node that shows a window of child starting at row offset. Measure reports the child's natural size (constrained); Render gives the child its full natural height and returns the s.H rows from offset, with offset clamped to [0, height-s.H] so the window never runs past the end. A child that implements Windowed is instead asked for only the s.H visible rows. Use it inside Fill for scrollable content. When child is a Column (not Windowed), its children wrapped in Sticky stay at the top of the viewport once scrolled past.

func Stack

func Stack(children ...Node) Node

Stack overlays its children on one another, all with their top-left corner at the Stack's own (0, 0) unless wrapped in Absolute. Children are drawn at their natural size (what Measure reports within the Stack's bounds), opaque over what is beneath them, in ascending z order: a child wrapped in Layer(z, n) with a higher z is drawn on top of one with a lower z where they overlap, and children of equal z (an unwrapped child has z 0) are drawn in the order given, so the later one is on top. Measure is the bounding box of the children. Layer and Absolute must be direct children of the Stack; a Named wrapper around them hides them from it. Stack is a CellNode, and Rects and NamedAt report its children in drawing order, so the topmost Named node is the last one that contains a cell.

func Sticky

func Sticky(n Node) Node

Sticky returns n marked as a sticky row for the Scroll it is a child of: n is a child of a Column that is the direct child of a Scroll (see Scroll), and while the Scroll is scrolled past it, n is drawn at the top of the viewport instead of scrolling away. Several sticky rows pin one under the other in order, and a sticky row hides what scrolls beneath it. Anywhere else Sticky is n.

func StyledText

func StyledText(text string, style ansi.Style, wrap bool) Node

StyledText returns a Node for a block of text. When wrap is true the text is word-wrapped (ansi.Wrap) to whatever width the node is given, and Measure reports the wrapped height for the width it may use; the style, if any, is applied to each line after wrapping so an escape sequence is never split. Render shows exactly the allotted Size, cutting rows beyond the height. Empty text measures as nothing.

func Text

func Text(s string, opts ...TextOpt) Node

Text returns a Node for a block of text. With WithWrap, Measure reports the wrapped height for the width it may use. Render shows exactly the allotted Size, cutting columns and rows beyond it. Empty text measures as nothing.

func ViewFunc

func ViewFunc(f func() string) Node

ViewFunc returns a Node that shows whatever f returns each time it is measured or rendered, cut to the allotted Size. It is for widgets whose View changes with time (a clock) or state, where capturing the string once would go stale.

type Placed

type Placed struct {
	// Node is the node, as it appears in the tree.
	Node Node
	// Name is the label given by Named, or "".
	Name string
	// Rect is where the node is drawn, relative to the root. It is clipped
	// to every ancestor, so a child that overflows its parent reports only
	// the visible part (an empty Rect if none of it shows).
	Rect Rect
	// Depth is 0 for the root, 1 for its children, and so on.
	Depth int
}

Placed is one node of a laid-out tree with the cells it occupies.

func Rects

func Rects(root Node, s Size) []Placed

Rects returns every node of the tree under root, laid out at size s, in depth-first pre-order (a parent before its children, siblings in order), with the cells each occupies. s is the size root is rendered at: pass root.Measure(c), the size Draw uses, or the Size given to Render.

The rectangles come from the same sizing that Render uses, so a position here is where that node's content appears in Render(s). Use it to build mouse regions (hittest.Map) from the layout instead of working the numbers out by hand. Coordinates are relative to root's top-left; add the view's own origin for terminal coordinates (in inline mode those are terminal-absolute).

Row, Column, BoxNode, GridNode and Named containers report their children; every other Node, including widget adapters, is a leaf. A node that appears twice in the tree is reported twice.

type Rect

type Rect struct{ X, Y, W, H int }

Rect is a rectangle of terminal cells: its top-left corner (X, Y) and its size, relative to the top-left corner of the root that Rects was given.

func RectOf

func RectOf(root Node, s Size, name string) (Rect, bool)

RectOf returns the Rect of the first node named name (see Named) in the tree under root laid out at size s, and false if there is none.

func (Rect) Contains

func (r Rect) Contains(x, y int) bool

Contains reports whether the cell (x, y) is inside r.

func (Rect) CutBottom

func (r Rect) CutBottom(h int) (bottom, rest Rect)

CutBottom splits r into its bottom h rows and the rest above them.

func (Rect) CutLeft

func (r Rect) CutLeft(w int) (left, rest Rect)

CutLeft splits r into its left w columns and the rest.

func (Rect) CutRight

func (r Rect) CutRight(w int) (right, rest Rect)

CutRight splits r into its right w columns and the rest to their left.

func (Rect) CutTop

func (r Rect) CutTop(h int) (top, rest Rect)

CutTop splits r into its top h rows and the rest. h is clamped to 0..r.H, so the two parts always tile r exactly.

func (Rect) Empty

func (r Rect) Empty() bool

Empty reports whether r covers no cells.

func (Rect) Local

func (r Rect) Local(x, y int) (lx, ly int)

Local converts screen cell (x, y) to coordinates relative to r's top-left corner. It does not check that the cell is inside r.

type Size

type Size struct{ W, H int }

Size is a width and height in terminal cells.

func DrawTo

func DrawTo(root Node, dst CellSurface, c Constraints) Size

DrawTo is Draw into a cell grid: it measures root under c, clears that many cells at dst's origin and draws root into them, returning the size drawn (the zero Size if either dimension is 0). The screen it produces is the one Draw's string would parse to. Like Draw it measures each built-in container's children once per distinct Constraints, and for a tree of CellNodes it allocates nothing per frame once warm.

type TextOpt

type TextOpt func(*textNode)

TextOpt configures a Text node.

func WithAlign

func WithAlign(a Align) TextOpt

WithAlign places each line within the width: AlignStart (the default) flush left, AlignCenter centred (an odd remainder goes after the line) and AlignEnd flush right. It does not change what Measure reports.

func WithEllipsis

func WithEllipsis(glyph string) TextOpt

WithEllipsis ends a line wider than the width with glyph instead of cutting it mid-word, so the reader can tell text was dropped. Pass the theme's Glyphs.Ellipsis so it follows the ASCII fallback; an empty glyph uses "~". The line, glyph included, is at most the width.

func WithStyle

func WithStyle(st ansi.Style) TextOpt

WithStyle applies st to every rendered line, after wrapping so an escape sequence is never split.

func WithWrap

func WithWrap() TextOpt

WithWrap word-wraps the text to the width the node is given. A word is only broken across lines when it is longer than that width. Without it, each line is clipped to the width.

type Track

type Track struct {
	Size int
	Grow int
}

Track describes one grid column's width behaviour. The zero value sizes the column to its widest cell. Size > 0 fixes the width to exactly Size cells (Grow is then ignored). Otherwise Grow > 0 shares the grid's leftover width by weight, on top of the column's content width.

type Windowed

type Windowed interface {
	Node
	// Rows is the total number of rows the node has at width w.
	Rows(w int) int
	// RenderRows returns rows [start, start+count) at width w, joined by
	// "\n". It is never asked for more rows than fit the viewport.
	RenderRows(w, start, count int) string
}

Windowed is implemented by a Node that can render a slice of its rows without producing the rest — a very long list or log. Scroll uses it to ask the child for only the visible window.

Jump to

Keyboard shortcuts

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