summaryrefslogtreecommitdiffhomepage
path: root/packages/tui/internal/layout/flex.go
blob: 5b10a952370edd30b321a80264595a8a3b2833f6 (plain)
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
package layout

import (
	"strings"

	"github.com/charmbracelet/lipgloss/v2"
	"github.com/charmbracelet/lipgloss/v2/compat"
	"github.com/sst/opencode/internal/styles"
	"github.com/sst/opencode/internal/theme"
)

type Direction int

const (
	Row Direction = iota
	Column
)

type Justify int

const (
	JustifyStart Justify = iota
	JustifyEnd
	JustifyCenter
	JustifySpaceBetween
	JustifySpaceAround
)

type Align int

const (
	AlignStart Align = iota
	AlignEnd
	AlignCenter
	AlignStretch // Only applicable in the cross-axis
)

type FlexOptions struct {
	Background *compat.AdaptiveColor
	Direction  Direction
	Justify    Justify
	Align      Align
	Width      int
	Height     int
	Gap        int
}

type FlexItem struct {
	View      string
	FixedSize int  // Fixed size in the main axis (width for Row, height for Column)
	Grow      bool // If true, the item will grow to fill available space
}

// Render lays out a series of view strings based on flexbox-like rules.
func Render(opts FlexOptions, items ...FlexItem) string {
	if len(items) == 0 {
		return ""
	}

	t := theme.CurrentTheme()
	if opts.Background == nil {
		background := t.Background()
		opts.Background = &background
	}

	// Calculate dimensions for each item
	mainAxisSize := opts.Width
	crossAxisSize := opts.Height
	if opts.Direction == Column {
		mainAxisSize = opts.Height
		crossAxisSize = opts.Width
	}

	// Calculate total fixed size and count grow items
	totalFixedSize := 0
	growCount := 0
	for _, item := range items {
		if item.FixedSize > 0 {
			totalFixedSize += item.FixedSize
		} else if item.Grow {
			growCount++
		}
	}

	// Account for gaps between items
	totalGapSize := 0
	if len(items) > 1 && opts.Gap > 0 {
		totalGapSize = opts.Gap * (len(items) - 1)
	}

	// Calculate available space for grow items
	availableSpace := max(mainAxisSize-totalFixedSize-totalGapSize, 0)

	// Calculate size for each grow item
	growItemSize := 0
	if growCount > 0 && availableSpace > 0 {
		growItemSize = availableSpace / growCount
	}

	// Prepare sized views
	sizedViews := make([]string, len(items))
	actualSizes := make([]int, len(items))

	for i, item := range items {
		view := item.View

		// Determine the size for this item
		itemSize := 0
		if item.FixedSize > 0 {
			itemSize = item.FixedSize
		} else if item.Grow && growItemSize > 0 {
			itemSize = growItemSize
		} else {
			// No fixed size and not growing - use natural size
			if opts.Direction == Row {
				itemSize = lipgloss.Width(view)
			} else {
				itemSize = lipgloss.Height(view)
			}
		}

		// Apply size constraints
		if opts.Direction == Row {
			// For row direction, constrain width and handle height alignment
			if itemSize > 0 {
				view = styles.NewStyle().
					Background(*opts.Background).
					Width(itemSize).
					Height(crossAxisSize).
					Render(view)
			}

			// Apply cross-axis alignment
			switch opts.Align {
			case AlignCenter:
				view = lipgloss.PlaceVertical(
					crossAxisSize,
					lipgloss.Center,
					view,
					styles.WhitespaceStyle(*opts.Background),
				)
			case AlignEnd:
				view = lipgloss.PlaceVertical(
					crossAxisSize,
					lipgloss.Bottom,
					view,
					styles.WhitespaceStyle(*opts.Background),
				)
			case AlignStart:
				view = lipgloss.PlaceVertical(
					crossAxisSize,
					lipgloss.Top,
					view,
					styles.WhitespaceStyle(*opts.Background),
				)
			case AlignStretch:
				// Already stretched by Height setting above
			}
		} else {
			// For column direction, constrain height and handle width alignment
			if itemSize > 0 {
				style := styles.NewStyle().
					Background(*opts.Background).
					Height(itemSize)
				// Only set width for stretch alignment
				if opts.Align == AlignStretch {
					style = style.Width(crossAxisSize)
				}
				view = style.Render(view)
			}

			// Apply cross-axis alignment
			switch opts.Align {
			case AlignCenter:
				view = lipgloss.PlaceHorizontal(
					crossAxisSize,
					lipgloss.Center,
					view,
					styles.WhitespaceStyle(*opts.Background),
				)
			case AlignEnd:
				view = lipgloss.PlaceHorizontal(
					crossAxisSize,
					lipgloss.Right,
					view,
					styles.WhitespaceStyle(*opts.Background),
				)
			case AlignStart:
				view = lipgloss.PlaceHorizontal(
					crossAxisSize,
					lipgloss.Left,
					view,
					styles.WhitespaceStyle(*opts.Background),
				)
			case AlignStretch:
				// Already stretched by Width setting above
			}
		}

		sizedViews[i] = view
		if opts.Direction == Row {
			actualSizes[i] = lipgloss.Width(view)
		} else {
			actualSizes[i] = lipgloss.Height(view)
		}
	}

	// Calculate total actual size including gaps
	totalActualSize := 0
	for _, size := range actualSizes {
		totalActualSize += size
	}
	if len(items) > 1 && opts.Gap > 0 {
		totalActualSize += opts.Gap * (len(items) - 1)
	}

	// Apply justification
	remainingSpace := max(mainAxisSize-totalActualSize, 0)

	// Calculate spacing based on justification
	var spaceBefore, spaceBetween, spaceAfter int
	switch opts.Justify {
	case JustifyStart:
		spaceAfter = remainingSpace
	case JustifyEnd:
		spaceBefore = remainingSpace
	case JustifyCenter:
		spaceBefore = remainingSpace / 2
		spaceAfter = remainingSpace - spaceBefore
	case JustifySpaceBetween:
		if len(items) > 1 {
			spaceBetween = remainingSpace / (len(items) - 1)
		} else {
			spaceAfter = remainingSpace
		}
	case JustifySpaceAround:
		if len(items) > 0 {
			spaceAround := remainingSpace / (len(items) * 2)
			spaceBefore = spaceAround
			spaceAfter = spaceAround
			spaceBetween = spaceAround * 2
		}
	}

	// Build the final layout
	var parts []string

	spaceStyle := styles.NewStyle().Background(*opts.Background)
	// Add space before if needed
	if spaceBefore > 0 {
		if opts.Direction == Row {
			space := strings.Repeat(" ", spaceBefore)
			parts = append(parts, spaceStyle.Render(space))
		} else {
			// For vertical layout, add empty lines as separate parts
			for range spaceBefore {
				parts = append(parts, "")
			}
		}
	}

	// Add items with spacing
	for i, view := range sizedViews {
		parts = append(parts, view)

		// Add space between items (not after the last one)
		if i < len(sizedViews)-1 {
			// Add gap first, then any additional spacing from justification
			totalSpacing := opts.Gap + spaceBetween
			if totalSpacing > 0 {
				if opts.Direction == Row {
					space := strings.Repeat(" ", totalSpacing)
					parts = append(parts, spaceStyle.Render(space))
				} else {
					// For vertical layout, add empty lines as separate parts
					for range totalSpacing {
						parts = append(parts, "")
					}
				}
			}
		}
	}

	// Add space after if needed
	if spaceAfter > 0 {
		if opts.Direction == Row {
			space := strings.Repeat(" ", spaceAfter)
			parts = append(parts, spaceStyle.Render(space))
		} else {
			// For vertical layout, add empty lines as separate parts
			for range spaceAfter {
				parts = append(parts, "")
			}
		}
	}

	// Join the parts
	if opts.Direction == Row {
		return lipgloss.JoinHorizontal(lipgloss.Top, parts...)
	} else {
		return lipgloss.JoinVertical(lipgloss.Left, parts...)
	}
}

// Helper function to create a simple vertical layout
func Vertical(width, height int, items ...FlexItem) string {
	return Render(FlexOptions{
		Direction: Column,
		Width:     width,
		Height:    height,
		Justify:   JustifyStart,
		Align:     AlignStretch,
	}, items...)
}

// Helper function to create a simple horizontal layout
func Horizontal(width, height int, items ...FlexItem) string {
	return Render(FlexOptions{
		Direction: Row,
		Width:     width,
		Height:    height,
		Justify:   JustifyStart,
		Align:     AlignStretch,
	}, items...)
}