Files
eSliderandCursor 09e242d7c3 refactor(office): generalize DataTable layout and document TUI table skill
Unify project/user flex column layout into layoutFlexTable with per-subject
policy in model. Add office-tui-table skill and AGENTS notes; fix CI go mod
tidy drift and test matrix Go version to match go.mod 1.25.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 11:21:20 +01:00

226 lines
5.1 KiB
Go

package ui
import (
"github.com/eslider/go-onlyoffice/cmd/office/model"
"github.com/mattn/go-runewidth"
)
type columnLayout struct {
indices []int
widths map[int]int
}
func (t *DataTable) computeLayout() columnLayout {
contentW := t.lineContentWidth()
if len(t.columns) == 0 || contentW <= 0 {
return columnLayout{widths: map[int]int{}}
}
if flex, ok := model.TableFlexLayoutFor(t.spec.Subject); ok {
return layoutFlexTable(t.columns, contentW, flex.FlexColumnKey, flex.MinFlexWidth)
}
return layoutScrollingTable(t.columns, t.colScroll, contentW)
}
const minFixedColumnWidth = 3
// layoutFlexTable shows every column; flexKey absorbs leftover width.
func layoutFlexTable(cols []model.TableColumn, totalW int, flexKey string, minFlexWidth int) columnLayout {
indices := make([]int, len(cols))
for i := range cols {
indices[i] = i
}
widths := make(map[int]int, len(cols))
flexIdx := -1
fixed := 0
for i, col := range cols {
if col.Key == flexKey {
flexIdx = i
continue
}
widths[i] = col.Width
fixed += widths[i]
}
if flexIdx < 0 {
return layoutScrollingTable(cols, 0, totalW)
}
flexW := totalW - fixed
if flexW < minFlexWidth {
shrinkFixedColumns(widths, indices, flexIdx, fixed+minFlexWidth-totalW)
fixed = 0
for _, i := range indices {
if i != flexIdx {
fixed += widths[i]
}
}
flexW = totalW - fixed
if flexW < minFlexWidth {
flexW = minFlexWidth
}
}
widths[flexIdx] = flexW
normalizeWidthSum(widths, indices, totalW, flexIdx)
return columnLayout{indices: indices, widths: widths}
}
func shrinkFixedColumns(widths map[int]int, indices []int, flexIdx, need int) {
for need > 0 {
changed := false
for _, i := range indices {
if i == flexIdx || widths[i] <= minFixedColumnWidth {
continue
}
widths[i]--
need--
changed = true
if need == 0 {
return
}
}
if !changed {
return
}
}
}
func layoutScrollingTable(cols []model.TableColumn, colScroll, contentW int) columnLayout {
indices := pickVisibleColumnIndices(cols, colScroll, contentW)
widths := make(map[int]int, len(indices))
if len(indices) == 0 {
return columnLayout{indices: indices, widths: widths}
}
minSum := 0
for _, i := range indices {
minSum += cols[i].Width
}
widths = distributeColumnWidths(minSum, contentW, indices, cols)
return columnLayout{indices: indices, widths: widths}
}
func pickVisibleColumnIndices(cols []model.TableColumn, colScroll, contentW int) []int {
var out []int
used := 0
for colIdx := colScroll; colIdx < len(cols); colIdx++ {
w := cols[colIdx].Width
if len(out) > 0 && used+w > contentW {
break
}
out = append(out, colIdx)
used += w
}
if len(out) == 0 {
colIdx := colScroll
if colIdx < 0 || colIdx >= len(cols) {
colIdx = 0
}
out = []int{colIdx}
}
return out
}
// distributeColumnWidths expands or shrinks visible columns to exactly fill total width.
func distributeColumnWidths(minSum, total int, indices []int, cols []model.TableColumn) map[int]int {
out := make(map[int]int, len(indices))
if len(indices) == 0 {
return out
}
if total < len(indices) {
total = len(indices)
}
if minSum <= 0 {
each := total / len(indices)
if each < 1 {
each = 1
}
for _, i := range indices {
out[i] = each
}
normalizeWidthSum(out, indices, total, indices[len(indices)-1])
return out
}
for _, i := range indices {
out[i] = cols[i].Width
}
if minSum >= total {
for _, i := range indices {
out[i] = cols[i].Width * total / minSum
if out[i] < 1 {
out[i] = 1
}
}
normalizeWidthSum(out, indices, total, flexColumnIndex(indices, cols))
return out
}
extra := total - minSum
flex := flexColumnIndices(indices, cols)
flexSum := 0
for _, i := range flex {
flexSum += cols[i].Width
}
if flexSum <= 0 {
flexSum = len(flex)
}
for _, i := range flex {
out[i] += extra * cols[i].Width / flexSum
}
normalizeWidthSum(out, indices, total, flexColumnIndex(indices, cols))
return out
}
func flexColumnIndices(indices []int, cols []model.TableColumn) []int {
flex := make([]int, 0, len(indices))
for _, i := range indices {
switch cols[i].Key {
case "title", "subtitle", "description", "displayName", "primaryEmail", "from", "to", "type":
flex = append(flex, i)
}
}
if len(flex) == 0 {
flex = append(flex, indices...)
}
return flex
}
func flexColumnIndex(indices []int, cols []model.TableColumn) int {
flex := flexColumnIndices(indices, cols)
if len(flex) == 0 {
return indices[len(indices)-1]
}
for _, i := range indices {
if cols[i].Key == "title" {
return i
}
}
return flex[0]
}
func normalizeWidthSum(widths map[int]int, indices []int, total, adjustIdx int) {
if len(indices) == 0 {
return
}
sum := 0
for _, i := range indices {
sum += widths[i]
}
if adjustIdx < 0 {
adjustIdx = indices[len(indices)-1]
}
widths[adjustIdx] += total - sum
if widths[adjustIdx] < 1 {
widths[adjustIdx] = 1
}
}
const cellHPadding = 0 // padding lives in outer lipgloss styles; column width is the full cell width
func truncateCellText(text string, colWidth int) string {
if colWidth < 1 {
colWidth = 1
}
if runewidth.StringWidth(text) <= colWidth {
return text
}
return runewidth.Truncate(text, colWidth, "…")
}