From 1543f3508342a0ec0c11f39d060cde101afb7b1e Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Sun, 17 May 2026 19:59:46 +0800 Subject: [PATCH] fix(table): resize columns when WithWidth is set WithWidth only changed the viewport width while headers and cells kept their configured column widths, causing misaligned layout and trailing padding (#696). Distribute or shrink column content widths so the rendered table matches the requested width, including default cell padding. Co-authored-by: Cursor --- table/resize.go | 106 ++++++++++++++++++ table/resize_test.go | 44 ++++++++ table/table.go | 42 +++++-- table/table_test.go | 2 +- .../TestModel_View/Extra_padding.golden | 14 +-- .../Width_greater_than_columns.golden | 8 +- .../Width_less_than_columns.golden | 14 +-- 7 files changed, 198 insertions(+), 32 deletions(-) create mode 100644 table/resize.go create mode 100644 table/resize_test.go diff --git a/table/resize.go b/table/resize.go new file mode 100644 index 000000000..651858044 --- /dev/null +++ b/table/resize.go @@ -0,0 +1,106 @@ +package table + +func (m Model) cellFrameWidth() int { + return max( + m.styles.Header.GetHorizontalFrameSize(), + m.styles.Cell.GetHorizontalFrameSize(), + ) +} + +func (m Model) effectiveColumnWidths() []int { + widths := make([]int, len(m.cols)) + for i, col := range m.cols { + widths[i] = col.Width + } + if m.tableWidth <= 0 { + return widths + } + + return resizeColumnWidths(widths, m.tableWidth, m.cellFrameWidth()) +} + +func resizeColumnWidths(widths []int, target, frame int) []int { + out := append([]int(nil), widths...) + + var indices []int + for i, w := range out { + if w > 0 { + indices = append(indices, i) + } + } + + n := len(indices) + if n == 0 { + return out + } + + sum := 0 + for _, i := range indices { + sum += out[i] + frame + } + if sum == target { + return out + } + + contentTarget := target - n*frame + if contentTarget < n { + contentTarget = n + } + + contentSum := 0 + for _, i := range indices { + contentSum += out[i] + } + + switch { + case contentSum < contentTarget: + extra := contentTarget - contentSum + for _, i := range indices { + add := extra * out[i] / contentSum + out[i] += add + extra -= add + } + for j := 0; extra > 0; j++ { + out[indices[j%len(indices)]]++ + extra-- + } + case contentSum > contentTarget: + remaining := contentTarget + for k, i := range indices { + if k == len(indices)-1 { + out[i] = max(1, remaining) + break + } + w := max(1, out[i]*contentTarget/contentSum) + out[i] = w + remaining -= w + } + } + + for { + sum = 0 + for _, i := range indices { + sum += out[i] + frame + } + if sum == target { + break + } + if sum < target { + out[indices[0]]++ + continue + } + shrunk := false + for _, i := range indices { + if out[i] > 1 { + out[i]-- + shrunk = true + break + } + } + if !shrunk { + break + } + } + + return out +} diff --git a/table/resize_test.go b/table/resize_test.go new file mode 100644 index 000000000..d2c6029a6 --- /dev/null +++ b/table/resize_test.go @@ -0,0 +1,44 @@ +package table + +import "testing" + +func TestResizeColumnWidthsExpand(t *testing.T) { + got := resizeColumnWidths([]int{25, 16, 12}, 80, 2) + sum := 0 + for _, w := range got { + sum += w + 2 + } + if sum != 80 { + t.Fatalf("total width %d, want 80, widths %v", sum, got) + } +} + +func TestResizeColumnWidthsShrink(t *testing.T) { + got := resizeColumnWidths([]int{25, 16, 12}, 30, 2) + sum := 0 + for _, w := range got { + sum += w + 2 + } + if sum != 30 { + t.Fatalf("total width %d, want 30, widths %v", sum, got) + } +} + +func TestModelViewWidthMatchesTableWidth(t *testing.T) { + m := New( + WithWidth(80), + WithColumns([]Column{ + {Title: "Name", Width: 25}, + {Title: "Country", Width: 16}, + {Title: "Dunk", Width: 12}, + }), + WithRows([]Row{{"foo", "UK", "Yes"}}), + ) + + if got := m.naturalWidth(); got != 59 { + t.Fatalf("naturalWidth %d want 59", got) + } + if w := m.Width(); w != 80 { + t.Fatalf("Width() %d want 80", w) + } +} diff --git a/table/table.go b/table/table.go index 39f9a4003..aeebf44f1 100644 --- a/table/table.go +++ b/table/table.go @@ -26,6 +26,8 @@ type Model struct { viewport viewport.Model start int end int + + tableWidth int } // Row represents one line in the table. @@ -173,7 +175,7 @@ func WithHeight(h int) Option { // WithWidth sets the width of the table. func WithWidth(w int) Option { return func(m *Model) { - m.viewport.SetWidth(w) + m.SetWidth(w) } } @@ -319,12 +321,28 @@ func (m *Model) SetColumns(c []Column) { m.UpdateViewport() } -// SetWidth sets the width of the viewport of the table. +// SetWidth sets the total rendered width of the table. func (m *Model) SetWidth(w int) { - m.viewport.SetWidth(w) + m.tableWidth = w + if w > 0 { + m.viewport.SetWidth(w) + } else { + m.viewport.SetWidth(m.naturalWidth()) + } m.UpdateViewport() } +func (m Model) naturalWidth() int { + frame := m.cellFrameWidth() + total := 0 + for _, col := range m.cols { + if col.Width > 0 { + total += col.Width + frame + } + } + return total +} + // SetHeight sets the height of the viewport of the table. func (m *Model) SetHeight(h int) { m.viewport.SetHeight(h - lipgloss.Height(m.headersView())) @@ -416,26 +434,30 @@ func (m *Model) FromValues(value, separator string) { } func (m Model) headersView() string { + widths := m.effectiveColumnWidths() s := make([]string, 0, len(m.cols)) - for _, col := range m.cols { - if col.Width <= 0 { + for i, col := range m.cols { + w := widths[i] + if w <= 0 { continue } - style := lipgloss.NewStyle().Width(col.Width).MaxWidth(col.Width).Inline(true) - renderedCell := style.Render(ansi.Truncate(col.Title, col.Width, "…")) + style := lipgloss.NewStyle().Width(w).MaxWidth(w).Inline(true) + renderedCell := style.Render(ansi.Truncate(col.Title, w, "…")) s = append(s, m.styles.Header.Render(renderedCell)) } return lipgloss.JoinHorizontal(lipgloss.Top, s...) } func (m *Model) renderRow(r int) string { + widths := m.effectiveColumnWidths() s := make([]string, 0, len(m.cols)) for i, value := range m.rows[r] { - if m.cols[i].Width <= 0 { + w := widths[i] + if w <= 0 { continue } - style := lipgloss.NewStyle().Width(m.cols[i].Width).MaxWidth(m.cols[i].Width).Inline(true) - renderedCell := m.styles.Cell.Render(style.Render(ansi.Truncate(value, m.cols[i].Width, "…"))) + style := lipgloss.NewStyle().Width(w).MaxWidth(w).Inline(true) + renderedCell := m.styles.Cell.Render(style.Render(ansi.Truncate(value, w, "…"))) s = append(s, renderedCell) } diff --git a/table/table_test.go b/table/table_test.go index b83f6458f..6daa0458b 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -130,6 +130,7 @@ func TestNew(t *testing.T) { viewport.WithWidth(10), viewport.WithHeight(20), ), + tableWidth: 10, }, }, "WithFocused": { @@ -737,7 +738,6 @@ func TestModel_View(t *testing.T) { }), ) }, - skip: true, }, "Modified viewport height": { modelFunc: func() Model { diff --git a/table/testdata/TestModel_View/Extra_padding.golden b/table/testdata/TestModel_View/Extra_padding.golden index 6afe1a9ed..982cecf0d 100644 --- a/table/testdata/TestModel_View/Extra_padding.golden +++ b/table/testdata/TestModel_View/Extra_padding.golden @@ -1,14 +1,14 @@ - - - Name Country of Orig… Dunk-able - - - Chocolate Digestives UK Yes + Name Country of Or… Dunk-able - Tim Tams Australia No + Chocolate Digestives UK Yes + + + + + Tim Tams Australia No \ No newline at end of file diff --git a/table/testdata/TestModel_View/Width_greater_than_columns.golden b/table/testdata/TestModel_View/Width_greater_than_columns.golden index 684450492..654e7f20d 100644 --- a/table/testdata/TestModel_View/Width_greater_than_columns.golden +++ b/table/testdata/TestModel_View/Width_greater_than_columns.golden @@ -1,7 +1,7 @@ - Name Country of Orig… Dunk-able - Chocolate Digestives UK Yes - Tim Tams Australia No - Hobnobs UK Yes + Name Country of Origin Dunk-able + Chocolate Digestives UK Yes + Tim Tams Australia No + Hobnobs UK Yes diff --git a/table/testdata/TestModel_View/Width_less_than_columns.golden b/table/testdata/TestModel_View/Width_less_than_columns.golden index d2bc25b96..5bf04724d 100644 --- a/table/testdata/TestModel_View/Width_less_than_columns.golden +++ b/table/testdata/TestModel_View/Width_less_than_columns.golden @@ -1,7 +1,7 @@ - Name Country of Origin Dunk-able - Chocolate Digestives UK Yes - Tim Tams Australia No - Hobnobs UK Yes + Name Countr… Dunk-… + Chocolate … UK Yes + Tim Tams Austra… No + Hobnobs UK Yes @@ -12,10 +12,4 @@ - - - - - - \ No newline at end of file