From 60b0b13dfada71cc19a193a336efdbe6bbb27c59 Mon Sep 17 00:00:00 2001 From: Charlie Tonneslan Date: Fri, 15 May 2026 10:18:25 -0400 Subject: [PATCH] fix(textinput): stop applying Text/Placeholder style to padding When Width is set, View pads the input out to that width with spaces. Both the text path and the placeholder path were running those padding spaces through the active style, so setting a background color on either Text or Placeholder caused the bg to extend well past the actual content, which doesn't match how a placeholder usually reads in CSS terms (you expect the style to scope to the text itself). Drop the styling on the padding in both code paths so the two views behave the same way: style covers the text, the rest is empty space. Closes #245. Signed-off-by: Charlie Tonneslan --- textinput/textinput.go | 13 ++++++++----- textinput/textinput_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/textinput/textinput.go b/textinput/textinput.go index 363089b2d..615f173fc 100644 --- a/textinput/textinput.go +++ b/textinput/textinput.go @@ -719,15 +719,16 @@ func (m Model) View() string { } } - // If a max width and background color were set fill the empty spaces with - // the background color. + // Pad the rest of the input out to its full width. The padding spaces are + // left unstyled so that Text styling stops at the value's end, matching + // the placeholder's behavior. See #245. valWidth := uniseg.StringWidth(string(value)) if m.Width() > 0 && valWidth <= m.Width() { padding := max(0, m.Width()-valWidth) if valWidth+padding <= m.Width() && pos < len(value) { padding++ } - v += styleText(strings.Repeat(" ", padding)) + v += strings.Repeat(" ", padding) } return m.promptView() + v @@ -768,9 +769,11 @@ func (m Model) placeholderView() string { minWidth += availWidth availWidth = 0 } - // append placeholder[len] - cursor, append padding + // append placeholder[len] - cursor, append padding (padding is + // intentionally unstyled so the placeholder color only covers the + // placeholder text itself, matching the text-mode behavior in View). v += render(string(p[1:minWidth])) - v += render(strings.Repeat(" ", availWidth)) + v += strings.Repeat(" ", availWidth) } else { // if there is no width, the placeholder can be any length v += render(string(p[1:])) diff --git a/textinput/textinput_test.go b/textinput/textinput_test.go index b5e344b99..bc9c03f5b 100644 --- a/textinput/textinput_test.go +++ b/textinput/textinput_test.go @@ -7,6 +7,7 @@ import ( "testing" tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" ) func Test_CurrentSuggestion(t *testing.T) { @@ -107,6 +108,36 @@ func ExampleValidateFunc() { } } +func TestTextStyleAndPlaceholderStyleScopeMatch(t *testing.T) { + // Regression test for #245. The padding spaces that fill the input out to + // its full width must not pick up the Text or Placeholder style, otherwise + // the user sees a styled background extending past the actual text. Both + // the text and placeholder code paths should agree. + bg := lipgloss.NewStyle().Background(lipgloss.Color("#AFAFAF")) + + m := New() + m.SetWidth(20) + m.Placeholder = "Pies" + styles := m.Styles() + styles.Focused.Text = bg + styles.Focused.Placeholder = bg + styles.Blurred.Text = bg + styles.Blurred.Placeholder = bg + m.SetStyles(styles) + m.Blur() + + placeholderView := m.View() + if !strings.HasSuffix(placeholderView, strings.Repeat(" ", 17)) { + t.Errorf("placeholder view should end with unstyled padding spaces, got %q", placeholderView) + } + + m.SetValue("foo") + textView := m.View() + if !strings.HasSuffix(textView, strings.Repeat(" ", 17)) { + t.Errorf("text view should end with unstyled padding spaces, got %q", textView) + } +} + func keyPress(key rune) tea.Msg { return tea.KeyPressMsg{Code: key, Text: string(key)} }