Files
terminal/tui/pane.go
T
Lixen d4cc9f2407 v0.5.2 tui: titles and labels drawn a column a rune; an unchanged frame writes nothing
* tui: titles, labels and buttons drawn a column a rune

Ten places drew each rune of a title or label at its byte offset, so a
string holding runes of more than one byte drew with holes and ran past the
width it was measured and truncated to: the Pane title, the Modal title and
hint, the Divider label, TabBar titles, Table cells, Form labels, the
TextField placeholder, the ConfirmDialog and AlertDialog buttons, and the
ProgressOverlay title. Each now draws through Region.Text, which counts
columns by rune, within the bound its loop kept; a Form label stays within
its label column, as the byte count meant it to.

* an unchanged frame writes nothing

flush closed every frame with SGR 0, so a frame that changed no cell still
sent four bytes, and an app drawing an idle screen kept writing to the
terminal. The reset now follows only a frame that wrote cells: the next
cell written restates its whole style, and Fini resets on its own.

Each widget, given a title such as "● é · ñ", draws it whole where it drew
it with holes before; a frame flushed twice writes nothing the second time,
where it wrote ESC [ 0 m.
2026-10-24 19:00:37 -04:00

63 lines
1.5 KiB
Go

package tui
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// PaneOpts configures pane rendering
type PaneOpts struct {
Title string
Border LineType
BorderFg color.RGB
Bg color.RGB
TitleFg color.RGB
}
// Pane draws bordered pane with optional title, returns content region
// Content region is inside border, below title row if present
func (r Region) Pane(opts PaneOpts) Region {
if r.W < 3 || r.H < 3 {
return r.Sub(1, 1, 0, 0)
}
// Fill background
r.Fill(opts.Bg)
// Draw border
r.Box(opts.Border, opts.BorderFg)
// Title on top edge
if opts.Title != "" {
title := " " + opts.Title + " "
if RuneLen(title) > r.W-4 {
title = Truncate(title, r.W-4)
}
r.Text(2, 0, title, opts.TitleFg, opts.Bg, terminal.AttrBold)
}
// Content region is everything inside the border. The title sits ON the border row.
return r.Sub(1, 1, r.W-2, r.H-2)
}
// TitledPane fills region with background, draws centered title at top, returns content region
// Content region starts at row 1 with full width
func (r Region) TitledPane(title string, titleFg, bg color.RGB) Region {
r.Fill(bg)
if title != "" && r.H > 0 {
r.TextCenter(0, title, titleFg, bg, terminal.AttrBold)
}
if r.H <= 1 {
return r.Sub(0, 0, r.W, 0)
}
return r.Sub(0, 1, r.W, r.H-1)
}
// TitledPaneFocused is TitledPane with focus-dependent background
func (r Region) TitledPaneFocused(title string, titleFg, bg, focusBg color.RGB, focused bool) Region {
if focused {
bg = focusBg
}
return r.TitledPane(title, titleFg, bg)
}