gitstack

grindlemire/go-tui code browser

18.4 KB markdown 618 lines 2026-03-23 ยท a47141f raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
# Layout Reference

## Overview

go-tui uses a CSS flexbox layout engine to position elements on screen. Every `<div>` acts as a flex container, arranging its children along a main axis (horizontal by default) with control over alignment, spacing, and sizing. The layout engine runs automatically when state changes, but you can also trigger it manually for testing or advanced use cases.

All layout types live in the root `tui` package (re-exported from `internal/layout`). You interact with them through element options in `.gsx` files or programmatically via `tui.New()`.

```gsx
// Flexbox in action: vertical layout with centered children
<div class="flex-col items-center gap-1 p-1 border-rounded">
    <span class="font-bold">Title</span>
    <span>Content goes here</span>
</div>
```

```go
// Programmatic equivalent
root := tui.New(
    tui.WithDirection(tui.Column),
    tui.WithAlign(tui.AlignCenter),
    tui.WithGap(1),
    tui.WithPadding(1),
    tui.WithBorder(tui.BorderRounded),
)
title := tui.New(
    tui.WithText("Title"),
    tui.WithTextStyle(tui.NewStyle().Bold()),
)
body := tui.New(tui.WithText("Content goes here"))
root.AddChild(title, body)
```

## Direction

`Direction` controls which axis children are laid out along. It is a `uint8` enum.

| Constant | Value | Description |
|----------|-------|-------------|
| `Row` | `0` | Children laid out left-to-right (default) |
| `Column` | `1` | Children laid out top-to-bottom |

```go
tui.New(tui.WithDirection(tui.Column)) // vertical stack
tui.New(tui.WithDirection(tui.Row))    // horizontal row (default)
```

In `.gsx`, use the Tailwind classes `flex-col` for column and `flex` or `flex-row` for row.

## Justify

`Justify` controls how children are distributed along the main axis (the direction axis). It is a `uint8` enum.

| Constant | Value | Description |
|----------|-------|-------------|
| `JustifyStart` | `0` | Pack children at the start (default) |
| `JustifyEnd` | `1` | Pack children at the end |
| `JustifyCenter` | `2` | Center children |
| `JustifySpaceBetween` | `3` | Equal space between children, no space at edges |
| `JustifySpaceAround` | `4` | Equal space around each child (half-space at edges) |
| `JustifySpaceEvenly` | `5` | Equal space between children and at edges |

```go
tui.New(tui.WithJustify(tui.JustifyCenter))       // center children
tui.New(tui.WithJustify(tui.JustifySpaceBetween))  // spread children apart
```

Visual comparison for three children `[A] [B] [C]` in a row:

```
JustifyStart:        [A][B][C]
JustifyEnd:                      [A][B][C]
JustifyCenter:            [A][B][C]
JustifySpaceBetween: [A]       [B]       [C]
JustifySpaceAround:   [A]    [B]    [C]
JustifySpaceEvenly:    [A]    [B]    [C]
```

In `.gsx`, use `justify-start`, `justify-center`, `justify-end`, `justify-between`, `justify-around`, or `justify-evenly`.

## Align

`Align` controls how children are positioned on the cross axis (perpendicular to the direction). It is a `uint8` enum.

| Constant | Value | Description |
|----------|-------|-------------|
| `AlignStart` | `0` | Align to the start of the cross axis |
| `AlignEnd` | `1` | Align to the end of the cross axis |
| `AlignCenter` | `2` | Center on the cross axis |
| `AlignStretch` | `3` | Stretch to fill the cross axis (default) |

```go
tui.New(tui.WithAlign(tui.AlignCenter))  // center children on cross axis
tui.New(tui.WithAlign(tui.AlignStretch)) // stretch to fill (default)
```

In `.gsx`, use `items-start`, `items-end`, `items-center`, or `items-stretch`.

Individual children can override the parent's alignment with `AlignSelf`:

```go
tui.New(tui.WithAlignSelf(tui.AlignCenter)) // this child centers itself
```

In `.gsx`, use `self-start`, `self-center`, `self-end`, or `self-stretch`.

## FlexWrap

`FlexWrap` controls whether children wrap onto new lines when they overflow the main axis. It is a `uint8` enum.

| Constant | Value | Description |
|----------|-------|-------------|
| `WrapNone` | `0` | Children stay on one line (default) |
| `Wrap` | `1` | Children wrap to new lines when they overflow |
| `WrapReverse` | `2` | Children wrap in reverse order |

```go
tui.New(tui.WithFlexWrap(tui.Wrap)) // enable wrapping
```

In `.gsx`, use `flex-wrap`, `flex-wrap-reverse`, or `flex-nowrap`.

With wrapping enabled, each line handles grow, shrink, and justify separately.

## AlignContent

`AlignContent` controls how wrapped lines are spaced along the cross axis. It requires `FlexWrap` to be enabled and at least two lines to have any effect. It is a `uint8` enum.

| Constant | Value | Description |
|----------|-------|-------------|
| `ContentStart` | `0` | Pack lines at the start (default) |
| `ContentEnd` | `1` | Pack lines at the end |
| `ContentCenter` | `2` | Center lines |
| `ContentStretch` | `3` | Stretch lines to fill the cross axis |
| `ContentSpaceBetween` | `4` | First line at start, last at end, even spacing between |
| `ContentSpaceAround` | `5` | Equal spacing around each line |

```go
tui.New(
    tui.WithFlexWrap(tui.Wrap),
    tui.WithAlignContent(tui.ContentCenter),
)
```

In `.gsx`, use `content-start`, `content-end`, `content-center`, `content-stretch`, `content-between`, or `content-around`.

## Value

`Value` represents a dimension that can be fixed, percentage-based, or automatic. The layout engine resolves values against available space during calculation.

### Constructors

```go
func Fixed(n int) Value
```

Creates a value representing an absolute number of terminal cells (columns for width, rows for height).

```go
tui.Fixed(40) // exactly 40 characters wide
```

---

```go
func Percent(p float64) Value
```

Creates a value representing a percentage of the parent's available space. Uses a 0-100 scale.

```go
tui.Percent(50)  // half of available space
tui.Percent(100) // full available space
```

---

```go
func Auto() Value
```

Creates a value that sizes to content. The layout engine computes the actual size from the element's intrinsic dimensions or flex properties. This is the default for both width and height.

```go
tui.Auto() // size determined by content
```

### Value Fields

```go
type Value struct {
    Amount float64
    Unit   Unit
}
```

| Field | Type | Description |
|-------|------|-------------|
| `Amount` | `float64` | The numeric value (cell count for Fixed, 0-100 for Percent, unused for Auto) |
| `Unit` | `Unit` | How the value is interpreted |

### Unit Constants

| Constant | Value | Description |
|----------|-------|-------------|
| `UnitAuto` | `0` | Size determined by content or flex |
| `UnitFixed` | `1` | Absolute terminal cells |
| `UnitPercent` | `2` | Percentage of parent's available space |

### Value Methods

#### Resolve

```go
func (v Value) Resolve(available, fallback int) int
```

Computes the actual integer value given the available space. For `UnitFixed`, returns the amount directly. For `UnitPercent`, computes `available * amount / 100`. For `UnitAuto`, returns the fallback value.

```go
v := tui.Percent(50)
cells := v.Resolve(80, 0) // returns 40 (50% of 80)

v = tui.Fixed(30)
cells = v.Resolve(80, 0) // returns 30

v = tui.Auto()
cells = v.Resolve(80, 12) // returns 12 (the fallback)
```

#### IsAuto

```go
func (v Value) IsAuto() bool
```

Returns `true` if the value has unit `UnitAuto`.

## LayoutStyle

`LayoutStyle` (aliased from `internal/layout.Style`) holds all layout properties for a node. You rarely construct this directly; instead, use element options like `WithWidth`, `WithDirection`, `WithPadding`, etc. However, it's useful for reading computed styles or building custom `Layoutable` implementations.

### Fields

```go
type LayoutStyle struct {
    // Sizing
    Width     Value
    Height    Value
    MinWidth  Value
    MinHeight Value
    MaxWidth  Value
    MaxHeight Value

    // Flex container properties
    Direction      Direction
    JustifyContent Justify
    AlignItems     Align
    Gap            int          // Space between children (main axis only)
    FlexWrap       FlexWrap     // Whether children wrap to new lines
    AlignContent   AlignContent // How wrapped lines are distributed on the cross axis

    // Flex item properties
    FlexGrow   float64      // How much to grow relative to siblings
    FlexShrink float64      // How much to shrink relative to siblings (default 1)
    AlignSelf  *Align       // Override parent's AlignItems (nil = inherit)

    // Spacing
    Padding Edges
    Margin  Edges
}
```

### DefaultLayoutStyle

```go
func DefaultLayoutStyle() LayoutStyle
```

Returns a `LayoutStyle` with the following defaults:

| Property | Default |
|----------|---------|
| `Width` | `Auto()` |
| `Height` | `Auto()` |
| `MinWidth` | `Auto()` (intrinsic size, matching CSS flexbox `min-width: auto`) |
| `MinHeight` | `Auto()` (intrinsic size) |
| `MaxWidth` | `Auto()` (no maximum) |
| `MaxHeight` | `Auto()` (no maximum) |
| `Direction` | `Row` |
| `JustifyContent` | `JustifyStart` |
| `AlignItems` | `AlignStretch` |
| `FlexWrap` | `WrapNone` |
| `AlignContent` | `ContentStart` |
| `FlexGrow` | `0` |
| `FlexShrink` | `1.0` |
| `AlignSelf` | `nil` (inherit from parent) |
| `Gap` | `0` |
| `Padding` | all zeros |
| `Margin` | all zeros |

## Rect

`Rect` represents a rectangle with integer coordinates. After layout runs, every element has a `Rect` (border box) and a `ContentRect` (inner area where children are placed).

### Fields

```go
type Rect struct {
    X, Y          int
    Width, Height int
}
```

`X` and `Y` are the top-left corner. `Width` and `Height` are the dimensions.

### NewRect

```go
func NewRect(x, y, width, height int) Rect
```

Creates a new Rect.

```go
r := tui.NewRect(5, 10, 40, 20) // x=5, y=10, 40 wide, 20 tall
```

### Edge Queries

```go
func (r Rect) Right() int
func (r Rect) Bottom() int
```

`Right()` returns `X + Width` (exclusive right edge). `Bottom()` returns `Y + Height` (exclusive bottom edge).

### State Queries

```go
func (r Rect) IsEmpty() bool
func (r Rect) Area() int
```

`IsEmpty()` returns `true` if width or height is zero or negative. `Area()` returns width times height (0 if empty).

### Hit Testing

```go
func (r Rect) Contains(x, y int) bool
func (r Rect) ContainsRect(other Rect) bool
```

`Contains` checks if a point falls inside the rectangle. Points on the left and top edges are inside; points on the right and bottom edges are outside. `ContainsRect` checks if another rectangle is fully within this one. An empty `other` is always contained.

```go
r := tui.NewRect(0, 0, 80, 24)
r.Contains(0, 0)    // true (top-left is inside)
r.Contains(80, 0)   // false (right edge is outside)
r.Contains(79, 23)  // true
```

### Geometric Operations

```go
func (r Rect) Inset(edges Edges) Rect
func (r Rect) Outset(edges Edges) Rect
func (r Rect) Translate(dx, dy int) Rect
func (r Rect) Intersect(other Rect) Rect
func (r Rect) Union(other Rect) Rect
func (r Rect) Intersects(other Rect) bool
```

| Method | Description |
|--------|-------------|
| `Inset(edges)` | Shrink the rectangle inward by the given edges. Positive values shrink; negative expand |
| `Outset(edges)` | Expand the rectangle outward by the given edges. The inverse of Inset |
| `Translate(dx, dy)` | Move the rectangle by `(dx, dy)` without changing size |
| `Intersect(other)` | Return the overlapping region. Returns an empty Rect if they don't overlap |
| `Union(other)` | Return the smallest rectangle containing both. If either is empty, returns the other |
| `Intersects(other)` | Return `true` if the rectangles overlap. Touching edges don't count |

```go
r := tui.NewRect(0, 0, 80, 24)

// Shrink by 2 on all sides
inner := r.Inset(tui.EdgeAll(2)) // {X:2, Y:2, Width:76, Height:20}

// Move right by 10
shifted := r.Translate(10, 0) // {X:10, Y:0, Width:80, Height:24}
```

### Clamp

```go
func (r Rect) Clamp(x, y int) (int, int)
```

Constrains a point to be within the rectangle bounds. Returns the clamped coordinates.

```go
r := tui.NewRect(0, 0, 80, 24)
x, y := r.Clamp(100, 30) // returns 79, 23
```

### Convenience Functions

```go
func InsetRect(r Rect, top, right, bottom, left int) Rect
func InsetUniform(r Rect, n int) Rect
```

`InsetRect` insets a Rect by TRBL amounts (CSS order). `InsetUniform` insets by the same amount on all edges. Both wrap `Rect.Inset()`.

```go
r := tui.NewRect(0, 0, 80, 24)
inner := tui.InsetUniform(r, 1)             // {X:1, Y:1, Width:78, Height:22}
inner = tui.InsetRect(r, 2, 1, 2, 1)        // {X:1, Y:2, Width:78, Height:20}
```

## Edges

`Edges` represents spacing values on the four sides of a box. Used for padding and margin.

### Fields

```go
type Edges struct {
    Top, Right, Bottom, Left int
}
```

### Constructors

```go
func EdgeAll(n int) Edges
```

Same value on all four sides.

```go
tui.EdgeAll(2) // {Top:2, Right:2, Bottom:2, Left:2}
```

---

```go
func EdgeSymmetric(v, h int) Edges
```

Vertical (top and bottom) and horizontal (left and right) values.

```go
tui.EdgeSymmetric(1, 2) // {Top:1, Right:2, Bottom:1, Left:2}
```

---

```go
func EdgeTRBL(t, r, b, l int) Edges
```

Per-side values in CSS order: Top, Right, Bottom, Left.

```go
tui.EdgeTRBL(1, 2, 3, 4) // {Top:1, Right:2, Bottom:3, Left:4}
```

### Methods

```go
func (e Edges) Horizontal() int
func (e Edges) Vertical() int
func (e Edges) IsZero() bool
```

| Method | Returns |
|--------|---------|
| `Horizontal()` | `Left + Right` |
| `Vertical()` | `Top + Bottom` |
| `IsZero()` | `true` if all four values are zero |

```go
e := tui.EdgeTRBL(1, 2, 1, 2)
e.Horizontal() // 4
e.Vertical()   // 2
e.IsZero()     // false
```

## Size

`Size` represents dimensions without a position.

### Fields

```go
type Size struct {
    Width, Height int
}
```

## Point

`Point` represents an (X, Y) coordinate.

### Fields

```go
type Point struct {
    X, Y int
}
```

### Methods

```go
func (p Point) Add(other Point) Point
func (p Point) Sub(other Point) Point
func (p Point) In(r Rect) bool
```

| Method | Description |
|--------|-------------|
| `Add(other)` | Return a new Point offset by `other` |
| `Sub(other)` | Return a new Point with `other` subtracted |
| `In(r)` | Return `true` if the point is inside the rectangle |

```go
a := tui.Point{X: 10, Y: 5}
b := tui.Point{X: 3, Y: 2}
c := a.Add(b) // {X: 13, Y: 7}

r := tui.NewRect(0, 0, 80, 24)
a.In(r) // true
```

## LayoutResult

`LayoutResult` (aliased from `internal/layout.Layout`) holds the computed position and size after the layout engine runs. Every element stores one of these after `Calculate` completes.

### Fields

```go
type LayoutResult struct {
    Rect        Rect     // Border box: full area including border and padding
    ContentRect Rect     // Content area: inside border and padding, where children go
    AbsoluteX   float64  // Float position before rounding (for jitter-free animation)
    AbsoluteY   float64  // Float position before rounding (for jitter-free animation)
}
```

`Rect` is the border box, the space allocated by the parent after margin. Use it for hit testing and bounds checking. `ContentRect` is the area inside border and padding where children are placed.

`AbsoluteX` and `AbsoluteY` store the true floating-point position before integer rounding. The layout engine tracks float positions through the tree and rounds only once at the end, preventing cumulative rounding errors that cause visual jitter during animations.

## Layoutable

`Layoutable` is the interface that nodes must implement to participate in layout calculation. `Element` implements this interface. You can also implement it on custom types to plug into the layout engine directly.

```go
type Layoutable interface {
    LayoutStyle() Style
    LayoutChildren() []Layoutable
    SetLayout(Layout)
    GetLayout() Layout
    IsDirty() bool
    SetDirty(dirty bool)
    IntrinsicSize() (width, height int)
}
```

| Method | Description |
|--------|-------------|
| `LayoutStyle()` | Returns the layout properties for this node |
| `LayoutChildren()` | Returns the children to be laid out (should exclude hidden children) |
| `SetLayout(Layout)` | Called by the engine to store computed layout |
| `GetLayout()` | Returns the last computed layout |
| `IsDirty()` | Whether this node needs recalculation |
| `SetDirty(bool)` | Mark or clear the dirty flag |
| `IntrinsicSize()` | Natural content-based dimensions. Used as the base size for auto-sized elements |

## Calculate

```go
func Calculate(root Layoutable, availableWidth, availableHeight int)
```

Runs the flexbox layout algorithm on the tree rooted at `root`. After this call, every node in the tree has its `LayoutResult` populated with computed positions and sizes. Only dirty nodes are recalculated (incremental layout).

The `availableWidth` and `availableHeight` are the outer constraints, typically the terminal dimensions.

```go
root := tui.New(
    tui.WithDirection(tui.Column),
    tui.WithText("Hello"),
)
tui.Calculate(root, 80, 24)

r := root.Rect()           // computed border box
cr := root.ContentRect()   // computed content area
```

During normal application use, the framework calls `Calculate` automatically before each render. You only need to call it directly when working with elements outside the app lifecycle (testing, offline layout computation, etc.).

### How the Algorithm Works

The layout engine implements CSS flexbox in several phases:

1. **Compute base sizes** -- Resolve each child's main-axis size from its style (fixed, percent, or intrinsic) and record its flex grow/shrink factor.
2. **Break into lines** -- If `FlexWrap` is enabled, split children into lines based on main-axis overflow. Each line runs phases 3 and 4 independently.
3. **Distribute free space** -- If there's leftover space, grow children proportional to their `FlexGrow`. If children overflow, shrink them proportional to their `FlexShrink`.
4. **Apply min/max constraints** -- Clamp each child's computed size to its min/max bounds. If clamping changes a child's size, redistribute the difference among remaining flexible children.
5. **Position on main axis** -- Place children along the main axis using the parent's `JustifyContent` setting to compute offsets and spacing.
6. **Distribute lines on cross axis** -- When wrapping produces multiple lines, use `AlignContent` to distribute them (start, end, center, stretch, space-between, space-around).
7. **Cross-axis sizing and alignment** -- Compute each child's cross-axis size. Stretch children if `AlignItems` is `AlignStretch` (unless overridden by `AlignSelf`). Position using the alignment setting.
8. **Recurse** -- Convert float positions to integer rects and recurse into each child's subtree.

The engine uses Yoga-style float rounding (described in [LayoutResult](#layoutresult)) to avoid jitter during animations.