Skip to content

Layout

Tachikoma uses a constraint-based layout system to divide terminal space into regions. You define constraints (fixed size, percentage, fill remaining space) and the layout engine computes the resulting rectangles.

Rect

Rect is the fundamental geometry type — a rectangle defined by position and size:

julia
r = Rect(x, y, width, height)   # 1-based coordinates

Geometric Helpers

julia
right(r)                         # r.x + r.width - 1
bottom(r)                        # r.y + r.height - 1
inner(r)                         # shrink by 1 on all sides (for borders)
margin(r; top=0, right=0, bottom=0, left=0)  # apply margins
shrink(r, n)                     # uniform margin of n cells
center(parent, width, height)    # center a rect inside parent
anchor(parent, w, h; h=:center, v=:center)   # anchor by h/v symbols

anchor supports h values :left, :center, :right and v values :top, :center, :bottom.

Constraints

Five constraint types control how space is divided:

TypeDescriptionExample
Fixed(n)Exactly n cellsFixed(20) — always 20 cells
Fill(w)Expand to fill remaining spaceFill() — weight 1 (default)
Percent(p)Percentage of total spacePercent(50) — half the space
Min(n)At least n cells, expands if availableMin(10)
Max(n)At most n cellsMax(40)

Fill accepts an optional weight: Fill(2) gets twice the remaining space of Fill(1).

julia
cols = split_layout(Layout(Horizontal, [Fixed(12), Percent(35), Fill()]), area)
for (col, label) in zip(cols, ["Fixed(12)", "Percent(35)", "Fill()"])
    inner = render(Block(), col, buf)
    lx = inner.x + max(0, (inner.width - length(label)) ÷ 2)
    set_string!(buf, lx, inner.y + inner.height ÷ 2, label, tstyle(:primary, bold=true))
end
layout_constraint_types example
julia
cols = split_layout(Layout(Horizontal, [Fill(1), Fill(2), Fill(3)]), area)
for (i, col) in enumerate(cols)
    label = "Fill($i)"
    inner = render(Block(), col, buf)
    lx = inner.x + max(0, (inner.width - length(label)) ÷ 2)
    set_string!(buf, lx, inner.y + inner.height ÷ 2, label, tstyle(:accent, bold=true))
    sz = "$(col.width) cols"
    lx2 = inner.x + max(0, (inner.width - length(sz)) ÷ 2)
    set_string!(buf, lx2, inner.y + inner.height ÷ 2 + 1, sz, tstyle(:text_dim))
end
layout_fill_weights example

Layout and split_layout

Create a Layout with a direction and constraints, then split a Rect:

julia
# Vertical split: header (3 rows) + body (flexible) + footer (1 row)
layout = Layout(Vertical, [Fixed(3), Fill(), Fixed(1)])
rects = split_layout(layout, area)
# rects[1] = header, rects[2] = body, rects[3] = footer

# Horizontal split: sidebar (25%) + main (75%)
layout = Layout(Horizontal, [Percent(25), Fill()])
cols = split_layout(layout, area)

Direction

Vertical splits top to bottom. Horizontal splits left to right.

julia
rows = split_layout(Layout(Vertical, [Fixed(3), Fill(), Fixed(2)]), area)
render(Block(title="Fixed(3)"), rows[1], buf)
render(Block(title="Fill()"), rows[2], buf)
render(Block(title="Fixed(2)"), rows[3], buf)
layout_vertical example
julia
cols = split_layout(Layout(Horizontal, [Percent(30), Fill()]), area)
render(Block(title="Percent(30)"), cols[1], buf)
render(Block(title="Fill()"), cols[2], buf)
layout_horizontal example

Alignment

julia
layout = Layout(Horizontal, [Fixed(20), Fixed(20)]; align=layout_center)
AlignmentBehavior
layout_startPack children at the start (default)
layout_centerCenter children in available space
layout_endPack children at the end
layout_space_betweenDistribute space evenly between children
julia
cols = split_layout(Layout(Horizontal, [Fixed(14), Fixed(14), Fixed(14)];
                           align=layout_space_between), area)
render(Block(title="Fixed(14)"), cols[1], buf)
render(Block(title="Fixed(14)"), cols[2], buf)
render(Block(title="Fixed(14)"), cols[3], buf)
set_string!(buf, area.x, bottom(area), " align=layout_space_between", tstyle(:text_dim))
layout_alignment example

Nesting Layouts

Build complex interfaces by nesting layouts:

julia
rows = split_layout(Layout(Vertical, [Fixed(3), Fill(), Fixed(1)]), area)
header, body, footer = rows[1], rows[2], rows[3]

cols = split_layout(Layout(Horizontal, [Percent(25), Fill()]), body)
sidebar, main = cols[1], cols[2]

render(Block(title="Header"), header, buf)
render(Block(title="Sidebar"), sidebar, buf)
render(Block(title="Main Content"), main, buf)
render(StatusBar(
    left=[Span("  Footer ", tstyle(:text_dim))],
    right=[Span("[q] quit ", tstyle(:text_dim))],
), footer, buf)
layout_nested example

ResizableLayout

ResizableLayout lets users drag pane borders with the mouse:

julia
@kwdef mutable struct MyApp <: Model
    layout::ResizableLayout = ResizableLayout(Horizontal, [Fixed(30), Fill()])
end

function update!(m::MyApp, evt::MouseEvent)
    handle_resize!(m.layout, evt) && return   # consumed by resize
    # ... handle other mouse events
end

function view(m::MyApp, f::Frame)
    buf = f.buffer
    rects = split_layout(m.layout, f.area)

    # Render panes
    render(left_widget, rects[1], buf)
    render(right_widget, rects[2], buf)

    # Draw resize handles (highlights border on hover)
    render_resize_handles!(buf, m.layout)
end

ResizableLayout API

julia
ResizableLayout(direction, constraints; min_pane_size=3)
split_layout(rl, rect)           # compute child rects
handle_resize!(rl, evt)          # process mouse drag, returns true if consumed
reset_layout!(rl)                # restore original constraints
render_resize_handles!(buf, rl)  # draw visual feedback on borders
  • Alt+click on a border rotates the layout direction (Horizontal ↔ Vertical)

  • Alt+drag a pane to swap it with another

  • Layout state is automatically persisted via Preferences.jl

julia
rl = ResizableLayout(Horizontal, [Fixed(20), Fill()])
rects = split_layout(rl, area)
render(Block(title="Left Pane"), rects[1], buf)
render(Block(title="Right Pane"), rects[2], buf)
render_resize_handles!(buf, rl)
set_string!(buf, rects[1].x + 1, bottom(rects[1]) - 1,
    "← drag border →", tstyle(:text_dim, dim=true))
layout_resizable example

Positioning

Layout divides space among multiple children. Positioning places a single widget within a space. The two compose naturally:

julia
rects = split_layout(Layout(Vertical, [Fixed(7), Fill()]), area)
render(bt, center(rects[1], bt), buf)    # centered widget in top slot
render(content, rects[2], buf)           # fill-style widget in bottom slot

center and anchor

center places a widget (or explicit dimensions) in the middle of a parent rect. anchor gives full control over horizontal and vertical alignment:

julia
center(parent, width, height)            # center explicit dimensions
center(parent, widget)                   # center using intrinsic_size

anchor(parent, w, h; h=:center, v=:center)   # explicit dimensions
anchor(parent, widget; h=:right, v=:bottom)  # widget intrinsic_size

anchor supports h values :left, :center, :right and v values :top, :center, :bottom.

anchor composes with margin for offset positioning:

julia
# Notification toast: centered horizontally, offset 1 row from top
toast_rect = anchor(margin(area; top=1), tw, 1; h=:center, v=:top)

# Status label: bottom-right corner with 1-cell margin
status_rect = anchor(margin(area; bottom=1, right=1), sw, 1; h=:right, v=:bottom)
julia
render(Block(), area, buf)
inner = Rect(area.x + 1, area.y + 1, area.width - 2, area.height - 2)

# center() — places a dialog in the middle of the content area
render(Block(title=" Dialog "), center(inner, 28, 5), buf)

# anchor() — overlays labels pinned to edges
for (h, v, label, sty) in [
    (:center, :top,    "✓ Saved",  tstyle(:success, bold=true)),
    (:right,  :bottom, "Ln 42 ",   tstyle(:text_dim)),
    (:left,   :bottom, " [?] ",    tstyle(:text_dim)),
]
    r = anchor(inner, length(label), 1; h=h, v=v)
    set_string!(buf, r.x, r.y, label, sty)
end
layout_center_anchor example

intrinsic_size protocol

Widgets with a natural size implement intrinsic_size(widget) -> (width, height). This lets center and anchor position them automatically:

Widgetintrinsic_sizeNotes
BigText(width, 5)3-wide glyphs + 1-cell gaps
Button(length(label) + 4, 1)Renders as "[ label ]"
Checkbox(length(label) + 4, 1)Renders as "[x] label"
RadioGroup(max_label_width + 2, n_labels)Marker + space + label per row
Calendar(22, 9)Fixed grid: 7 cols x 3 chars + header rows

Space-filling widgets (Gauge, Paragraph, Chart, Table, etc.) return nothing – they expand to fill whatever rect they are given.

To add intrinsic_size to a custom widget:

julia
struct MyWidget
    text::String
end
Tachikoma.intrinsic_size(w::MyWidget) = (length(w.text), 1)

Container

Container groups widgets with a layout for automatic positioning:

julia
container = Container(
    [widget1, widget2, widget3],
    Layout(Vertical, [Fixed(3), Fill(), Fixed(1)])
)
render(container, area, buf)

Each child is rendered into its corresponding layout rect. Pass three arguments to wrap in a Block:

julia
container = Container(
    [widget1, widget2],
    Layout(Horizontal, [Percent(50), Fill()]),
    Block(title="Panel"),
)
julia
container = Container(
    [Block(title="Alpha"), Block(title="Beta"), Block(title="Gamma")],
    Layout(Horizontal, [Fill(), Fill(), Fill()]),
    Block(title="Container"),
)
render(container, area, buf)
layout_container example