fix(tui): fill the frame background so the terminal backdrop can't bleed through

The main render path never applied the Screen backdrop, so any sub-view that left a line short or a row empty showed the transparent terminal background. Pad every frame to full width×height with the theme backdrop before display; content stays top-left anchored so caret coords are unaffected.
This commit is contained in:
2026-07-02 13:26:42 +04:00
parent 883e93cecb
commit 5deb2f6425
+26 -2
View File
@@ -41,14 +41,38 @@ const cursorMarker = ""
// bubbletea v2 the View carries terminal state, so renderRaw() builds the string // bubbletea v2 the View carries terminal state, so renderRaw() builds the string
// and View() resolves the caret to a real blinking cursor + sets the window title. // and View() resolves the caret to a real blinking cursor + sets the window title.
func (m Model) View() tea.View { func (m Model) View() tea.View {
content, cur := splitCursor(m.renderRaw(), " ") content, cur := splitCursor(m.fillFrame(m.renderRaw()), " ")
return tea.View{Content: content, AltScreen: true, Cursor: cur, WindowTitle: m.windowTitle()} return tea.View{Content: content, AltScreen: true, Cursor: cur, WindowTitle: m.windowTitle()}
} }
// fillFrame guarantees every cell of the width×height frame carries the theme backdrop, so a
// sub-view that leaves a line short or a row empty shows the theme background rather than the
// terminal-transparent backdrop bleeding through. Pads each line to full width and appends any
// missing bottom rows; content stays anchored top-left so caret coordinates are unaffected.
func (m Model) fillFrame(s string) string {
if m.width <= 0 || m.height <= 0 {
return s
}
pad := m.theme.Screen
lines := strings.Split(s, "\n")
out := make([]string, 0, m.height)
for i := 0; i < m.height; i++ {
ln := ""
if i < len(lines) {
ln = lines[i]
}
if gap := m.width - ansi.StringWidth(ln); gap > 0 {
ln += pad.Render(strings.Repeat(" ", gap))
}
out = append(out, ln)
}
return strings.Join(out, "\n")
}
// render returns the frame as a plain string with the caret drawn as the static // render returns the frame as a plain string with the caret drawn as the static
// "▏" glyph — for the preview tool and golden tests, which have no live cursor. // "▏" glyph — for the preview tool and golden tests, which have no live cursor.
func (m Model) render() string { func (m Model) render() string {
content, _ := splitCursor(m.renderRaw(), "▏") content, _ := splitCursor(m.fillFrame(m.renderRaw()), "▏")
return content return content
} }