gitstack

grindlemire/go-tui code browser

7.4 KB Go 293 lines 2026-02-26 · 056c696 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
package layout

import "math"

// layoutTable performs table-specific layout for a <table> element.
// It arranges children as rows (tr) containing cells (td/th) in a grid,
// computing column widths from the maximum intrinsic width of cells in each column
// and row heights from the maximum intrinsic height of cells in each row.
func layoutTable(table Layoutable, contentRect Rect, parentAbsX, parentAbsY float64) {
	rows := table.LayoutChildren()
	if len(rows) == 0 {
		return
	}

	// 1. Collect grid dimensions: determine number of columns and gather cells.
	numCols := 0
	for _, row := range rows {
		cells := row.LayoutChildren()
		if len(cells) > numCols {
			numCols = len(cells)
		}
	}
	if numCols == 0 {
		return
	}

	// 2. Compute column widths: max intrinsic width per column.
	// An explicit (non-Auto) width on a cell overrides its intrinsic width.
	colWidths := make([]int, numCols)
	colIsAuto := make([]bool, numCols) // track which columns are auto-sized
	for i := range colIsAuto {
		colIsAuto[i] = true
	}

	for _, row := range rows {
		cells := row.LayoutChildren()
		for ci, cell := range cells {
			cellStyle := cell.LayoutStyle()
			intrW, _ := cell.IntrinsicSize()

			var cellWidth int
			if !cellStyle.Width.IsAuto() {
				// Explicit width overrides intrinsic
				cellWidth = cellStyle.Width.Resolve(contentRect.Width, intrW)
				colIsAuto[ci] = false
			} else {
				cellWidth = intrW
			}

			// Include cell padding in column width calculation
			cellWidth += cellStyle.Padding.Horizontal()

			if cellWidth > colWidths[ci] {
				colWidths[ci] = cellWidth
			}
		}
	}

	// 3. Shrink auto columns proportionally if total > available width.
	// Include 1-character gap between columns in total width.
	columnGap := max(0, numCols-1) // 1 char gap between each pair of columns
	totalWidth := columnGap
	for _, w := range colWidths {
		totalWidth += w
	}

	if totalWidth > contentRect.Width {
		// Compute total auto-column width for proportional shrinking
		totalAutoWidth := 0
		for ci, w := range colWidths {
			if colIsAuto[ci] {
				totalAutoWidth += w
			}
		}

		overflow := totalWidth - contentRect.Width
		if totalAutoWidth > 0 && overflow > 0 {
			// Shrink auto columns proportionally
			shrunk := 0
			lastAutoCol := -1
			for ci := range colWidths {
				if colIsAuto[ci] {
					lastAutoCol = ci
				}
			}

			for ci := range colWidths {
				if colIsAuto[ci] {
					reduction := int(float64(overflow) * float64(colWidths[ci]) / float64(totalAutoWidth))
					if ci == lastAutoCol {
						// Give the remainder to the last auto column to avoid rounding errors
						reduction = overflow - shrunk
					}
					colWidths[ci] = max(1, colWidths[ci]-reduction)
					shrunk += reduction
				}
			}
		}
	}

	// 4. Compute row heights: max intrinsic height per row.
	// Explicit h-N on <tr> overrides the computed max.
	rowHeights := make([]int, len(rows))
	for ri, row := range rows {
		rowStyle := row.LayoutStyle()
		if !rowStyle.Height.IsAuto() {
			// Explicit row height overrides cell-based calculation
			rowHeights[ri] = rowStyle.Height.Resolve(contentRect.Height, 1)
			continue
		}

		cells := row.LayoutChildren()
		maxH := 1 // minimum row height is 1
		for _, cell := range cells {
			cellStyle := cell.LayoutStyle()
			_, intrH := cell.IntrinsicSize()

			var cellHeight int
			if !cellStyle.Height.IsAuto() {
				cellHeight = cellStyle.Height.Resolve(contentRect.Height, intrH)
			} else {
				cellHeight = intrH
			}

			// Include cell padding in row height calculation
			cellHeight += cellStyle.Padding.Vertical()

			if cellHeight > maxH {
				maxH = cellHeight
			}
		}
		rowHeights[ri] = maxH
	}

	// 5. Position rows top-to-bottom, cells left-to-right at column offsets.
	// Precompute column X offsets with 1-character gap between columns.
	colOffsets := make([]float64, numCols)
	offset := 0.0
	for ci := range numCols {
		colOffsets[ci] = offset
		offset += float64(colWidths[ci]) + 1 // +1 for inter-column gap
	}

	rowAbsY := parentAbsY
	for ri, row := range rows {
		rowH := rowHeights[ri]

		// Set row layout
		rowAbsX := parentAbsX
		rowRect := Rect{
			X:      int(math.Round(rowAbsX)),
			Y:      int(math.Round(rowAbsY)),
			Width:  contentRect.Width,
			Height: rowH,
		}
		row.SetLayout(Layout{
			Rect:        rowRect,
			ContentRect: rowRect, // rows have no padding of their own
			AbsoluteX:   rowAbsX,
			AbsoluteY:   rowAbsY,
		})
		row.SetDirty(false)

		// Position cells within this row
		cells := row.LayoutChildren()
		for ci, cell := range cells {
			cellW := colWidths[ci]
			cellAbsX := parentAbsX + colOffsets[ci]
			cellAbsY := rowAbsY

			cellStyle := cell.LayoutStyle()

			// Border box for the cell
			cellBorderBox := Rect{
				X:      int(math.Round(cellAbsX)),
				Y:      int(math.Round(cellAbsY)),
				Width:  cellW,
				Height: rowH,
			}

			// Content rect: border box minus padding
			cellContentAbsX := cellAbsX + float64(cellStyle.Padding.Left)
			cellContentAbsY := cellAbsY + float64(cellStyle.Padding.Top)
			cellContentRect := Rect{
				X:      int(math.Round(cellContentAbsX)),
				Y:      int(math.Round(cellContentAbsY)),
				Width:  cellW - cellStyle.Padding.Horizontal(),
				Height: rowH - cellStyle.Padding.Vertical(),
			}

			// Clamp content dimensions to non-negative
			if cellContentRect.Width < 0 {
				cellContentRect.Width = 0
			}
			if cellContentRect.Height < 0 {
				cellContentRect.Height = 0
			}

			cell.SetLayout(Layout{
				Rect:        cellBorderBox,
				ContentRect: cellContentRect,
				AbsoluteX:   cellAbsX,
				AbsoluteY:   cellAbsY,
			})
			cell.SetDirty(false)

			// 6. Recurse into cell children using the flex layout
			cellChildren := cell.LayoutChildren()
			if len(cellChildren) > 0 {
				layoutChildren(cell, cellContentRect, cellContentAbsX, cellContentAbsY)
			}
		}

		rowAbsY += float64(rowH)
	}
}

// TableIntrinsicSize computes the intrinsic size of a table.
// Width = sum of max column widths, Height = sum of max row heights.
func TableIntrinsicSize(table Layoutable) (width, height int) {
	rows := table.LayoutChildren()
	if len(rows) == 0 {
		return 0, 0
	}

	// Determine number of columns
	numCols := 0
	for _, row := range rows {
		cells := row.LayoutChildren()
		if len(cells) > numCols {
			numCols = len(cells)
		}
	}
	if numCols == 0 {
		return 0, 0
	}

	// Compute column widths (max intrinsic width per column)
	colWidths := make([]int, numCols)
	for _, row := range rows {
		cells := row.LayoutChildren()
		for ci, cell := range cells {
			cellStyle := cell.LayoutStyle()
			intrW, _ := cell.IntrinsicSize()

			var cellWidth int
			if !cellStyle.Width.IsAuto() {
				cellWidth = cellStyle.Width.Resolve(0, intrW)
			} else {
				cellWidth = intrW
			}
			cellWidth += cellStyle.Padding.Horizontal()

			if cellWidth > colWidths[ci] {
				colWidths[ci] = cellWidth
			}
		}
	}

	// Compute row heights (max intrinsic height per row)
	for ri, row := range rows {
		cells := row.LayoutChildren()
		maxH := 1 // minimum row height is 1
		for _, cell := range cells {
			cellStyle := cell.LayoutStyle()
			_, intrH := cell.IntrinsicSize()

			var cellHeight int
			if !cellStyle.Height.IsAuto() {
				cellHeight = cellStyle.Height.Resolve(0, intrH)
			} else {
				cellHeight = intrH
			}
			cellHeight += cellStyle.Padding.Vertical()

			if cellHeight > maxH {
				maxH = cellHeight
			}
		}
		height += maxH
		_ = ri
	}

	// Sum column widths + inter-column gaps
	for _, w := range colWidths {
		width += w
	}
	if numCols > 1 {
		width += numCols - 1 // 1 char gap between each pair of columns
	}

	return width, height
}