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 ¶
- Constants
- func Draw(root Node, c Constraints) string
- func DrawTight(root Node, size Size) string
- func JoinHorizontalJustify(width int, justify Justify, blocks ...string) string
- func NamedAt(root Node, s Size, x, y int) (name string, relX, relY int, ok bool)
- func Overlay(base, overlay string, x, y int) string
- func Window(lines []string, cursor, h int) []string
- func WindowRange(total, cursor, h int) (start, end int)
- type Align
- type Border
- type Box
- func (b Box) Background(c ansi.Color) Box
- func (b Box) Border(border Border) Box
- func (b Box) BorderColor(c ansi.Color) Box
- func (b Box) BorderSides(top, right, bottom, left bool) Box
- func (b Box) Height(h int) Box
- func (b Box) Margin(top, right, bottom, left int) Box
- func (b Box) Padding(top, right, bottom, left int) Box
- func (b Box) PaddingAll(n int) Box
- func (b Box) Render(content string) stringdeprecated
- func (b Box) Title(s string, a Align) Box
- func (b Box) Width(w int) Box
- type Break
- type CellNode
- type CellSurface
- type Constraints
- type CrossAlign
- type FlexChild
- type Justify
- type Length
- type Node
- func Absolute(x, y int, n Node) Node
- func Block(s string) Node
- func BoxNode(box Box, child Node) Node
- func Column(gap int, children ...FlexChild) Node
- func ColumnJustify(gap int, justify Justify, children ...FlexChild) Node
- func Fixed(n Node, size Size) Node
- func GridNode(tracks []Track, gap int, cells ...Node) Node
- func GridNodeGaps(tracks []Track, colGap, rowGap int, cells ...Node) Node
- func Layer(z int, n Node) Node
- func MinMax(n Node, min, max Size) Node
- func MinSize(n Node, min Size, fallback Node) Node
- func Named(name string, n Node) Node
- func OverlayNode(base, over Node, x, y int) Node
- func Responsive(breaks []Break, nodes ...Node) Node
- func Row(gap int, children ...FlexChild) Node
- func RowJustify(gap int, justify Justify, children ...FlexChild) Node
- func Scroll(child Node, offset int) Node
- func Stack(children ...Node) Node
- func Sticky(n Node) Node
- func StyledText(text string, style ansi.Style, wrap bool) Node
- func Text(s string, opts ...TextOpt) Node
- func ViewFunc(f func() string) Node
- type Placed
- type Rect
- type Size
- type TextOpt
- type Track
- type Windowed
Examples ¶
Constants ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 RoundedBorder ¶
func RoundedBorder() Border
RoundedBorder draws rounded corners with thin 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 ¶
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 ¶
Border sets the border style (e.g. layout.NormalBorder()); the zero value draws no border.
func (Box) BorderColor ¶
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 ¶
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 ¶
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 ¶
Margin sets blank space outside the border, per side. It is not coloured by Background. Negative values count as 0.
func (Box) Padding ¶
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 ¶
PaddingAll sets n as the padding on all four sides.
func (Box) Render
deprecated
func (Box) Title ¶
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.
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 ¶
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 ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
ColumnJustify is Column with main-axis (vertical) Justify, as RowJustify is to Row.
func Fixed ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
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 ¶
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.
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 ¶
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 ¶
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.
type Track ¶
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.