diff --git a/textarea/textarea.go b/textarea/textarea.go index 1d3aa97..6421681 100644 --- a/textarea/textarea.go +++ b/textarea/textarea.go @@ -20,7 +20,6 @@ import ( const ( minHeight = 1 - minWidth = 2 defaultHeight = 6 defaultWidth = 40 defaultCharLimit = 400 @@ -862,32 +861,40 @@ 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) { - if m.MaxWidth > 0 { - m.viewport.Width = clamp(w, minWidth, m.MaxWidth) - } else { - m.viewport.Width = max(w, minWidth) - } - - // 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)) - } - - // Account for base style borders and padding. - inputWidth -= m.style.Base.GetHorizontalFrameSize() - + // 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) } - inputWidth -= m.promptWidth - if m.MaxWidth > 0 { - m.width = clamp(inputWidth, minWidth, m.MaxWidth) - } else { - m.width = max(inputWidth, minWidth) + // Add base style borders and padding to reserved outer width. + reservedOuter := m.style.Base.GetHorizontalFrameSize() + + // Add prompt width to reserved inner width. + reservedInner := m.promptWidth + + // Add line number width to reserved inner width. + if m.ShowLineNumbers { + const lnWidth = 4 // Up to 3 digits for line number plus 1 margin. + reservedInner += lnWidth } + + // Input width must be at least one more than the reserved inner and outer + // width. This gives us a minimum input width of 1. + minWidth := reservedInner + reservedOuter + 1 + inputWidth := max(w, minWidth) + + // Input width must be no more than maximum width. + if m.MaxWidth > 0 { + inputWidth = min(inputWidth, m.MaxWidth) + } + + // 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 + // the reserved width from them. + + m.viewport.Width = inputWidth - reservedOuter + m.width = inputWidth - reservedOuter - reservedInner } // SetPromptFunc supersedes the Prompt field and sets a dynamic prompt @@ -1163,7 +1170,7 @@ func (m Model) getPromptString(displayLine int) (prompt string) { func (m Model) placeholderView() string { var ( s strings.Builder - p = rw.Truncate(m.Placeholder, m.width, "...") + p = rw.Truncate(rw.Truncate(m.Placeholder, m.width, "..."), m.width, "") style = m.style.Placeholder.Inline(true) ) diff --git a/textarea/textarea_test.go b/textarea/textarea_test.go index e02ecf8..d0381a7 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,606 @@ 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 min width minus one", + modelFunc: func(m Model) Model { + m.SetWidth(6) + + input := "123" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + > 1 1 + > 2 + > 3 + > + > ~ + > ~ + `), + cursorRow: 3, + cursorCol: 0, + }, + }, + { + name: "set width min width", + modelFunc: func(m Model) Model { + m.SetWidth(7) + + input := "123" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + > 1 1 + > 2 + > 3 + > + > ~ + > ~ + `), + cursorRow: 3, + cursorCol: 0, + }, + }, + { + name: "set width min width no line numbers", + modelFunc: func(m Model) Model { + m.ShowLineNumbers = false + m.SetWidth(0) + + input := "123" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + > 1 + > 2 + > 3 + > + > ~ + > ~ + `), + cursorRow: 3, + cursorCol: 0, + }, + }, + { + name: "set width min width no line numbers no prompt", + modelFunc: func(m Model) Model { + m.ShowLineNumbers = false + m.Prompt = "" + m.SetWidth(0) + + input := "123" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + 1 + 2 + 3 + + ~ + ~ + `), + cursorRow: 3, + cursorCol: 0, + }, + }, + { + name: "set width min width plus one", + modelFunc: func(m Model) Model { + m.SetWidth(8) + + input := "123" + m = sendString(m, input) + + return m + }, + want: want{ + view: heredoc.Doc(` + > 1 12 + > 3 + > ~ + > ~ + > ~ + > ~ + `), + 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, + }, + }, + { + name: "placeholder min width", + modelFunc: func(m Model) Model { + m.SetWidth(0) + + return m + }, + want: want{ + view: heredoc.Doc(` + > 1 . + > ~ + > ~ + > ~ + > ~ + > ~ + `), + }, + }, } for _, tt := range tests {