diff --git a/textarea/textarea.go b/textarea/textarea.go index 1d3aa97b..4d59b741 100644 --- a/textarea/textarea.go +++ b/textarea/textarea.go @@ -862,32 +862,41 @@ func (m *Model) moveToEnd() { // It is important that the width of the textarea be exactly the given width // and no more. func (m *Model) SetWidth(w int) { + // Input width must be at least minimum width. + inputWidth := max(w, minWidth) + + // Input width must be no more than maximum width. if m.MaxWidth > 0 { - m.viewport.Width = clamp(w, minWidth, m.MaxWidth) - } else { - m.viewport.Width = max(w, minWidth) + inputWidth = min(inputWidth, m.MaxWidth) } - // Since the width of the textarea input is dependent on the width of the - // prompt and line numbers, we need to calculate it by subtracting. - inputWidth := w - if m.ShowLineNumbers { - inputWidth -= uniseg.StringWidth(fmt.Sprintf(m.lineNumberFormat, 0)) - } + // Since the width of the viewport and input area is dependent on the width of + // borders, prompt and line numbers, we need to calculate it by subtracting + // them from the desired width. - // Account for base style borders and padding. + // Subtract base style borders and padding. We need to handle this before we + // can set the viewport size. inputWidth -= m.style.Base.GetHorizontalFrameSize() + // Viewport width can now be set because we known the visible width. + m.viewport.Width = inputWidth + + // Subtract line number width from input width. + if m.ShowLineNumbers { + const lnWidth = 4 // Up to 3 digits for line number plus 1 margin. + inputWidth -= lnWidth + } + + // Update prompt width only if there is no prompt function as SetPromptFunc + // updates the prompt width when it is called. if m.promptFunc == nil { m.promptWidth = uniseg.StringWidth(m.Prompt) } + // Subtract prompt width from input width. inputWidth -= m.promptWidth - if m.MaxWidth > 0 { - m.width = clamp(inputWidth, minWidth, m.MaxWidth) - } else { - m.width = max(inputWidth, minWidth) - } + + m.width = inputWidth } // SetPromptFunc supersedes the Prompt field and sets a dynamic prompt diff --git a/textarea/textarea_test.go b/textarea/textarea_test.go index e02ecf8f..8f06ff57 100644 --- a/textarea/textarea_test.go +++ b/textarea/textarea_test.go @@ -8,6 +8,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/acarl005/stripansi" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) func TestVerticalScrolling(t *testing.T) { @@ -672,6 +673,470 @@ func TestView(t *testing.T) { cursorCol: 7, }, }, + { + name: "set width", + modelFunc: func(m Model) Model { + m.SetWidth(10) + + input := "12" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + > 1 12 + > ~ + > ~ + > ~ + > ~ + > ~ + `), + cursorRow: 0, + cursorCol: 2, + }, + }, + { + name: "set width max length text minus one", + modelFunc: func(m Model) Model { + m.SetWidth(10) + + input := "123" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + > 1 123 + > ~ + > ~ + > ~ + > ~ + > ~ + `), + cursorRow: 0, + cursorCol: 3, + }, + }, + { + name: "set width max length text", + modelFunc: func(m Model) Model { + m.SetWidth(10) + + input := "1234" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + > 1 1234 + > + > ~ + > ~ + > ~ + > ~ + `), + cursorRow: 1, + cursorCol: 0, + }, + }, + { + name: "set width max length text plus one", + modelFunc: func(m Model) Model { + m.SetWidth(10) + + input := "12345" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + > 1 1234 + > 5 + > ~ + > ~ + > ~ + > ~ + `), + cursorRow: 1, + cursorCol: 1, + }, + }, + { + name: "set width set max width minus one", + modelFunc: func(m Model) Model { + m.MaxWidth = 10 + m.SetWidth(11) + + input := "123" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + > 1 123 + > ~ + > ~ + > ~ + > ~ + > ~ + `), + cursorRow: 0, + cursorCol: 3, + }, + }, + { + name: "set width set max width", + modelFunc: func(m Model) Model { + m.MaxWidth = 10 + m.SetWidth(11) + + input := "1234" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + > 1 1234 + > + > ~ + > ~ + > ~ + > ~ + `), + cursorRow: 1, + cursorCol: 0, + }, + }, + { + name: "set width set max width plus one", + modelFunc: func(m Model) Model { + m.MaxWidth = 10 + m.SetWidth(11) + + input := "12345" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + > 1 1234 + > 5 + > ~ + > ~ + > ~ + > ~ + `), + cursorRow: 1, + cursorCol: 1, + }, + }, + { + name: "set width without line numbers max length text minus one", + modelFunc: func(m Model) Model { + m.ShowLineNumbers = false + m.SetWidth(6) + + input := "123" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + > 123 + > ~ + > ~ + > ~ + > ~ + > ~ + `), + cursorRow: 0, + cursorCol: 3, + }, + }, + { + name: "set width without line numbers max length text", + modelFunc: func(m Model) Model { + m.ShowLineNumbers = false + m.SetWidth(6) + + input := "1234" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + > 1234 + > + > ~ + > ~ + > ~ + > ~ + `), + cursorRow: 1, + cursorCol: 0, + }, + }, + { + name: "set width without line numbers max length text plus one", + modelFunc: func(m Model) Model { + m.ShowLineNumbers = false + m.SetWidth(6) + + input := "12345" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + > 1234 + > 5 + > ~ + > ~ + > ~ + > ~ + `), + cursorRow: 1, + cursorCol: 1, + }, + }, + { + name: "set width with style", + modelFunc: func(m Model) Model { + m.FocusedStyle.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder()) + m.Focus() + + m.SetWidth(12) + + input := "1" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + ┌──────────┐ + │> 1 1 │ + │> ~ │ + │> ~ │ + │> ~ │ + │> ~ │ + │> ~ │ + └──────────┘ + `), + cursorRow: 0, + cursorCol: 1, + }, + }, + { + name: "set width with style max width minus one", + modelFunc: func(m Model) Model { + m.FocusedStyle.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder()) + m.Focus() + + m.SetWidth(12) + + input := "123" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + ┌──────────┐ + │> 1 123 │ + │> ~ │ + │> ~ │ + │> ~ │ + │> ~ │ + │> ~ │ + └──────────┘ + `), + cursorRow: 0, + cursorCol: 3, + }, + }, + { + name: "set width with style max width", + modelFunc: func(m Model) Model { + m.FocusedStyle.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder()) + m.Focus() + + m.SetWidth(12) + + input := "1234" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + ┌──────────┐ + │> 1 1234│ + │> │ + │> ~ │ + │> ~ │ + │> ~ │ + │> ~ │ + └──────────┘ + `), + cursorRow: 1, + cursorCol: 0, + }, + }, + { + name: "set width with style max width plus one", + modelFunc: func(m Model) Model { + m.FocusedStyle.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder()) + m.Focus() + + m.SetWidth(12) + + input := "12345" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + ┌──────────┐ + │> 1 1234│ + │> 5 │ + │> ~ │ + │> ~ │ + │> ~ │ + │> ~ │ + └──────────┘ + `), + cursorRow: 1, + cursorCol: 1, + }, + }, + { + name: "set width without line numbers with style", + modelFunc: func(m Model) Model { + m.FocusedStyle.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder()) + m.Focus() + + m.ShowLineNumbers = false + m.SetWidth(12) + + input := "123456" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + ┌──────────┐ + │> 123456 │ + │> ~ │ + │> ~ │ + │> ~ │ + │> ~ │ + │> ~ │ + └──────────┘ + `), + cursorRow: 0, + cursorCol: 6, + }, + }, + { + name: "set width without line numbers with style max width minus one", + modelFunc: func(m Model) Model { + m.FocusedStyle.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder()) + m.Focus() + + m.ShowLineNumbers = false + m.SetWidth(12) + + input := "1234567" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + ┌──────────┐ + │> 1234567 │ + │> ~ │ + │> ~ │ + │> ~ │ + │> ~ │ + │> ~ │ + └──────────┘ + `), + cursorRow: 0, + cursorCol: 7, + }, + }, + { + name: "set width without line numbers with style max width", + modelFunc: func(m Model) Model { + m.FocusedStyle.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder()) + m.Focus() + + m.ShowLineNumbers = false + m.SetWidth(12) + + input := "12345678" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + ┌──────────┐ + │> 12345678│ + │> │ + │> ~ │ + │> ~ │ + │> ~ │ + │> ~ │ + └──────────┘ + `), + cursorRow: 1, + cursorCol: 0, + }, + }, + { + name: "set width without line numbers with style max width plus one", + modelFunc: func(m Model) Model { + m.FocusedStyle.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder()) + m.Focus() + + m.ShowLineNumbers = false + m.SetWidth(12) + + input := "123456789" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + ┌──────────┐ + │> 12345678│ + │> 9 │ + │> ~ │ + │> ~ │ + │> ~ │ + │> ~ │ + └──────────┘ + `), + cursorRow: 1, + cursorCol: 1, + }, + }, } for _, tt := range tests {