Add inline header/header-lines/footer borders inside the list frame

Adds a new BorderShape, BorderInline, accepted as a value for
--header-border, --header-lines-border, and --footer-border. When the
surrounding --list-border has both top and bottom horizontals (rounded,
sharp, bold, double, horizontal), the corresponding section is rendered
inside the list frame separated from the list content by a horizontal
line whose endpoints join the list border as T-junctions. Without a
compatible list border, the shape falls back to BorderLine.

Supports:
  - All three layouts (default, reverse, reverse-list).
  - Any combination of the three inline sections, producing stacked
    separators.
  - --header-label and --footer-label rendered on their separator row
    (and redrawn on reqRedrawHeaderLabel / reqRedrawFooterLabel).
  - Section-specific border colors on the separator line, with the
    T-junction characters painted in the list-border color so the outer
    frame stays visually continuous.

Rejects the combinations that do not make sense:
  - --input-border=inline / --list-border=inline / --preview-border=inline
  - --header-first + (--header-border=inline | --header-lines-border=inline)
  - --header-border=inline with a non-inline --header-lines-border
    (inline has to propagate inward toward the list content).
This commit is contained in:
Junegunn Choi
2026-04-18 13:11:48 +09:00
parent f56bdd2ca9
commit e873128d93
8 changed files with 409 additions and 36 deletions
+19
View File
@@ -1,6 +1,25 @@
CHANGELOG
=========
0.72.0
------
- `--header-border`, `--header-lines-border`, and `--footer-border` now accept
a new `inline` style that embeds the section inside the list frame,
separated from the list content by a horizontal line whose endpoints join
the surrounding list border as T-junctions.
- Requires `--list-border` with a line-drawing shape (rounded / sharp /
bold / double / horizontal); falls back to `line` otherwise.
- Works in every layout and supports stacking, e.g.
`--header-border=inline --header-lines-border=inline --footer-border=inline`
produces up to three internal separators inside the list frame.
- `--header-label` and `--footer-label` render on their respective
separator row.
- The separator inherits `--color list-border` when the section's own
border color is not explicitly set.
- `--header-first` is not compatible with `--header-border=inline` or
`--header-lines-border=inline`; `--header-border=inline` requires
`--header-lines-border` to be `inline` or unset.
0.71.0
------
_Release highlights: https://junegunn.github.io/fzf/releases/0.71.0/_
+14 -2
View File
@@ -1100,7 +1100,13 @@ Print header before the prompt line. When both normal header and header lines
.TP
.BI "\-\-header\-border" [=STYLE]
Draw border around the header section. \fBline\fR style draws a single
separator line between the header window and the list section.
separator line between the header window and the list section. \fBinline\fR
style embeds the header inside the list border frame, joined to the list
section by a horizontal separator with T-junctions; it requires a
line-drawing \fB\-\-list\-border\fR (rounded / sharp / bold / double /
horizontal) and falls back to \fBline\fR otherwise. Not compatible with
\fB\-\-header\-first\fR, and when \fB\-\-header\-lines\fR is also set
\fB\-\-header\-lines\-border\fR must also be \fBinline\fR.
.TP
.BI "\-\-header\-label" [=LABEL]
@@ -1116,6 +1122,9 @@ Display header from \fB--header\-lines\fR with a separate border. Pass
\fBnone\fR to still separate the header lines but without a border. To combine
two headers, use \fB\-\-no\-header\-lines\-border\fR. \fBline\fR style draws
a single separator line between the header lines and the list section.
\fBinline\fR style embeds the header lines inside the list border frame
with a T-junction separator; it requires a line-drawing
\fB\-\-list\-border\fR and is not compatible with \fB\-\-header\-first\fR.
.SS FOOTER
@@ -1129,7 +1138,10 @@ are not affected by \fB\-\-with\-nth\fR. ANSI color codes are processed even whe
.TP
.BI "\-\-footer\-border" [=STYLE]
Draw border around the footer section. \fBline\fR style draws a single
separator line between the footer and the list section.
separator line between the footer and the list section. \fBinline\fR style
embeds the footer inside the list border frame with a T-junction separator;
it requires a line-drawing \fB\-\-list\-border\fR and falls back to
\fBline\fR otherwise.
.TP
.BI "\-\-footer\-label" [=LABEL]
+19 -1
View File
@@ -953,6 +953,8 @@ func parseBorder(str string, optional bool) (tui.BorderShape, error) {
switch str {
case "line":
return tui.BorderLine, nil
case "inline":
return tui.BorderInline, nil
case "rounded":
return tui.BorderRounded, nil
case "sharp":
@@ -983,7 +985,7 @@ func parseBorder(str string, optional bool) (tui.BorderShape, error) {
if optional && str == "" {
return defaultBorderShape, nil
}
return tui.BorderNone, errors.New("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|horizontal|vertical|top|bottom|left|right|none)")
return tui.BorderNone, errors.New("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|horizontal|vertical|top|bottom|left|right|line|inline|none)")
}
func parseKeyChords(str string, message string) (map[tui.Event]string, []tui.Event, error) {
@@ -3610,6 +3612,22 @@ func validateOptions(opts *Options) error {
return errors.New("only ANSI attributes are allowed for 'nth' (regular, bold, underline, reverse, dim, italic, strikethrough)")
}
if opts.BorderShape == tui.BorderInline ||
opts.ListBorderShape == tui.BorderInline ||
opts.InputBorderShape == tui.BorderInline ||
opts.Preview.border == tui.BorderInline {
return errors.New("inline border is only supported for --header-border, --header-lines-border, and --footer-border")
}
if opts.HeaderFirst && (opts.HeaderBorderShape == tui.BorderInline || opts.HeaderLinesShape == tui.BorderInline) {
return errors.New("--header-first is not compatible with --header-border=inline or --header-lines-border=inline")
}
if opts.HeaderBorderShape == tui.BorderInline &&
opts.HeaderLinesShape != tui.BorderInline &&
opts.HeaderLinesShape != tui.BorderUndefined &&
opts.HeaderLinesShape != tui.BorderNone {
return errors.New("--header-border=inline requires --header-lines-border to be inline or unset")
}
return nil
}
+203 -28
View File
@@ -456,6 +456,11 @@ type Terminal struct {
proxyScript string
numLinesCache map[int32]numLinesCacheValue
raw bool
// Separator rows (relative to wborder.Top()) for inline sections. -1 when not inline.
inlineHeaderSepRow int
inlineHeaderLinesSepRow int
inlineFooterSepRow int
}
type numLinesCacheValue struct {
@@ -1227,6 +1232,22 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
}
}
// Inline borders are embedded between the list's top and bottom horizontals.
// Shapes missing either one (none/phantom/line/single-sided) fall back to a plain
// horizontal separator (same as BorderLine).
inlineSupported := t.listBorderShape.HasTop() && t.listBorderShape.HasBottom()
if !inlineSupported {
if t.headerBorderShape == tui.BorderInline {
t.headerBorderShape = tui.BorderLine
}
if t.headerLinesShape == tui.BorderInline {
t.headerLinesShape = tui.BorderLine
}
if t.footerBorderShape == tui.BorderInline {
t.footerBorderShape = tui.BorderLine
}
}
// Determine header border shape
if t.headerBorderShape == tui.BorderLine {
if t.layout == layoutReverse {
@@ -2353,6 +2374,36 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
}
}
// Inline borders live inside the list frame instead of consuming shift/shrink/availableLines.
// Tracked here and applied when positioning t.window and the component windows.
const (
inlineRoleHeader = 1
inlineRoleHeaderLines = 2
inlineRoleFooter = 3
)
type inlineSlot struct {
role int
windowType tui.WindowType
contentLines int
}
var inlineTop []inlineSlot // ordered outer-to-inner (index 0 = closest to top border of wborder)
var inlineBottom []inlineSlot // ordered outer-to-inner (index 0 = closest to bottom border of wborder)
// Helper: reserve space inside wborder for a component. isInner indicates whether the section
// is adjacent to the list content (true) or to the outer list border (false).
addInline := func(onTop bool, contentLines int, windowType tui.WindowType, role int, isInner bool) {
slot := inlineSlot{role: role, windowType: windowType, contentLines: contentLines}
target := &inlineTop
if !onTop {
target = &inlineBottom
}
if isInner {
*target = append(*target, slot)
} else {
*target = append([]inlineSlot{slot}, *target...)
}
}
// Adjust position and size of the list window if header border is set
headerBorderHeight := 0
if hasHeaderWindow {
@@ -2360,37 +2411,69 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
if hasHeaderLinesWindow {
headerWindowHeight -= t.headerLines
}
headerBorderHeight = util.Constrain(borderLines(t.headerBorderShape)+headerWindowHeight, 0, availableLines)
if t.layout == layoutReverse {
shift += headerBorderHeight
shrink += headerBorderHeight
if t.headerBorderShape == tui.BorderInline {
// Reverse: header on top of list frame. Default/reverseList: header below.
// Header is the outer section (furthest from list) when paired with header-lines.
onTop := t.layout == layoutReverse
addInline(onTop, headerWindowHeight, tui.WindowHeader, inlineRoleHeader, false)
} else {
shrink += headerBorderHeight
headerBorderHeight = util.Constrain(borderLines(t.headerBorderShape)+headerWindowHeight, 0, availableLines)
if t.layout == layoutReverse {
shift += headerBorderHeight
shrink += headerBorderHeight
} else {
shrink += headerBorderHeight
}
availableLines -= headerBorderHeight
}
availableLines -= headerBorderHeight
}
headerLinesHeight := 0
if hasHeaderLinesWindow {
headerLinesHeight = util.Constrain(borderLines(headerLinesShape)+t.headerLines, 0, availableLines)
if t.layout != layoutDefault {
shift += headerLinesHeight
shrink += headerLinesHeight
if headerLinesShape == tui.BorderInline {
// Header-lines always sits adjacent to list content (inner).
// Reverse/reverseList: above list; default: below.
onTop := t.layout != layoutDefault
addInline(onTop, t.headerLines, tui.WindowHeader, inlineRoleHeaderLines, true)
} else {
shrink += headerLinesHeight
headerLinesHeight = util.Constrain(borderLines(headerLinesShape)+t.headerLines, 0, availableLines)
if t.layout != layoutDefault {
shift += headerLinesHeight
shrink += headerLinesHeight
} else {
shrink += headerLinesHeight
}
availableLines -= headerLinesHeight
}
availableLines -= headerLinesHeight
}
footerBorderHeight := 0
if hasFooterWindow {
// Footer lines should not take all available lines
footerBorderHeight = util.Constrain(borderLines(t.footerBorderShape)+len(t.footer), 0, availableLines)
shrink += footerBorderHeight
if t.layout != layoutReverse {
shift += footerBorderHeight
if t.footerBorderShape == tui.BorderInline {
// Reverse: footer below list (alone, inner). Default: footer above list (alone, inner).
// ReverseList: footer above list, outer of header-lines when header-lines is also inline.
onTop := t.layout != layoutReverse
isInner := t.layout != layoutReverseList
addInline(onTop, len(t.footer), tui.WindowFooter, inlineRoleFooter, isInner)
} else {
// Footer lines should not take all available lines
footerBorderHeight = util.Constrain(borderLines(t.footerBorderShape)+len(t.footer), 0, availableLines)
shrink += footerBorderHeight
if t.layout != layoutReverse {
shift += footerBorderHeight
}
availableLines -= footerBorderHeight
}
availableLines -= footerBorderHeight
}
// Compute total rows consumed inside wborder for inline sections (content + 1 separator per section).
inlineTopLines := 0
for _, s := range inlineTop {
inlineTopLines += s.contentLines + 1
}
inlineBottomLines := 0
for _, s := range inlineBottom {
inlineBottomLines += s.contentLines + 1
}
// Set up list border
@@ -2501,12 +2584,12 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
if previewOpts.position == posUp {
innerBorderFn(marginInt[0]+pheight, marginInt[3], width, height-pheight)
t.window = t.tui.NewWindow(
innerMarginInt[0]+pheight+shift, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink, tui.WindowList, noBorder, true)
innerMarginInt[0]+pheight+shift+inlineTopLines, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
} else {
innerBorderFn(marginInt[0], marginInt[3], width, height-pheight)
t.window = t.tui.NewWindow(
innerMarginInt[0]+shift, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink, tui.WindowList, noBorder, true)
innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
}
case posLeft, posRight:
@@ -2545,7 +2628,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
m = 1
}
t.window = t.tui.NewWindow(
innerMarginInt[0]+shift, innerMarginInt[3]+pwidth+m, innerWidth-pwidth-m, innerHeight-shrink, tui.WindowList, noBorder, true)
innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3]+pwidth+m, innerWidth-pwidth-m, innerHeight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
// Clear characters on the margin
// fzf --bind 'space:toggle-preview' --preview ':' --preview-window left,1
@@ -2577,7 +2660,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
}
innerBorderFn(marginInt[0], marginInt[3], width-pwidth, height)
t.window = t.tui.NewWindow(
innerMarginInt[0]+shift, innerMarginInt[3], innerWidth-pwidth, innerHeight-shrink, tui.WindowList, noBorder, true)
innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3], innerWidth-pwidth, innerHeight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
x := marginInt[3] + width - pwidth
createPreviewWindow(marginInt[0], x, pwidth, height)
}
@@ -2615,10 +2698,66 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
}
innerBorderFn(marginInt[0], marginInt[3], width, height)
t.window = t.tui.NewWindow(
innerMarginInt[0]+shift,
innerMarginInt[0]+shift+inlineTopLines,
innerMarginInt[3],
innerWidth,
innerHeight-shrink, tui.WindowList, noBorder, true)
innerHeight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
}
// Place inline section windows and draw T-junction separators across wborder.
// Also record each separator's wborder-relative row so labels can be printed on it.
t.inlineHeaderSepRow = -1
t.inlineHeaderLinesSepRow = -1
t.inlineFooterSepRow = -1
recordSep := func(role int, sepRow int) {
switch role {
case inlineRoleHeader:
t.inlineHeaderSepRow = sepRow
case inlineRoleHeaderLines:
t.inlineHeaderLinesSepRow = sepRow
case inlineRoleFooter:
t.inlineFooterSepRow = sepRow
}
}
if len(inlineTop)+len(inlineBottom) > 0 && t.wborder != nil {
// Inline sub-windows align with t.window horizontally (preview narrows t.window too)
// and stack immediately above or below it inside wborder.
subLeft := t.window.Left()
subWidth := t.window.Width()
cursor := t.window.Top() - inlineTopLines
for _, s := range inlineTop {
win := t.tui.NewWindow(cursor, subLeft, subWidth, s.contentLines, s.windowType, noBorder, true)
switch s.role {
case inlineRoleHeader:
t.headerWindow = win
case inlineRoleHeaderLines:
t.headerLinesWindow = win
case inlineRoleFooter:
t.footerWindow = win
}
cursor += s.contentLines
sepRow := cursor - t.wborder.Top()
t.wborder.DrawHSeparator(sepRow, s.windowType)
recordSep(s.role, sepRow)
cursor++
}
cursor = t.window.Top() + t.window.Height() + inlineBottomLines - 1
for _, s := range inlineBottom {
top := cursor - s.contentLines + 1
win := t.tui.NewWindow(top, subLeft, subWidth, s.contentLines, s.windowType, noBorder, true)
switch s.role {
case inlineRoleHeader:
t.headerWindow = win
case inlineRoleHeaderLines:
t.headerLinesWindow = win
case inlineRoleFooter:
t.footerWindow = win
}
sepRow := top - 1 - t.wborder.Top()
t.wborder.DrawHSeparator(sepRow, s.windowType)
recordSep(s.role, sepRow)
cursor = top - 2
}
}
if len(t.scrollbar) == 0 {
@@ -2700,7 +2839,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
}
// Set up header border
if hasHeaderWindow {
if hasHeaderWindow && t.headerBorderShape != tui.BorderInline {
var btop int
if hasInputWindow && t.headerFirst {
if t.layout == layoutReverse {
@@ -2728,7 +2867,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
}
// Set up header lines border
if hasHeaderLinesWindow {
if hasHeaderLinesWindow && headerLinesShape != tui.BorderInline {
var btop int
// NOTE: We still have to handle --header-first here in case
// --header-lines-border is set. Can't we just use header window instead
@@ -2761,7 +2900,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
}
// Set up footer
if hasFooterWindow {
if hasFooterWindow && t.footerBorderShape != tui.BorderInline {
var btop int
if t.layout == layoutReverse {
btop = w.Top() + w.Height()
@@ -2785,6 +2924,31 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
t.printLabel(t.inputBorder, t.inputLabel, t.inputLabelOpts, t.inputLabelLen, t.inputBorderShape, false)
t.printLabel(t.headerBorder, t.headerLabel, t.headerLabelOpts, t.headerLabelLen, t.headerBorderShape, false)
t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, false)
// Labels for inline sections render on the separator row of wborder.
if t.wborder != nil {
if t.inlineHeaderSepRow >= 0 {
t.printInlineLabel(t.wborder, t.inlineHeaderSepRow, t.headerLabel, t.headerLabelOpts, t.headerLabelLen)
}
if t.inlineFooterSepRow >= 0 {
t.printInlineLabel(t.wborder, t.inlineFooterSepRow, t.footerLabel, t.footerLabelOpts, t.footerLabelLen)
}
}
}
func (t *Terminal) printInlineLabel(window tui.Window, row int, render labelPrinter, opts labelOpts, length int) {
if window == nil || render == nil || window.Height() == 0 {
return
}
var col int
if opts.column == 0 {
col = max(0, (window.Width()-length)/2)
} else if opts.column < 0 {
col = max(0, window.Width()+opts.column+1-length)
} else {
col = min(opts.column-1, window.Width()-length)
}
window.Move(row, col)
render(window, window.Width())
}
func (t *Terminal) printLabel(window tui.Window, render labelPrinter, opts labelOpts, length int, borderShape tui.BorderShape, redrawBorder bool) {
@@ -3277,7 +3441,10 @@ func (t *Terminal) headerIndentImpl(base int, borderShape tui.BorderShape) int {
if t.listBorderShape.HasLeft() {
indentSize += 1 + t.borderWidth
}
if borderShape.HasLeft() {
// Section borders with their own left side skip past the list border's left column.
// Inline sections also skip it, but only when the list border actually has a left,
// since otherwise the inline window starts flush with the list window.
if borderShape.HasLeft() || (borderShape == tui.BorderInline && t.listBorderShape.HasLeft()) {
indentSize -= 1 + t.borderWidth
if indentSize < 0 {
indentSize = 0
@@ -5953,8 +6120,16 @@ func (t *Terminal) Loop() error {
t.printLabel(t.inputBorder, t.inputLabel, t.inputLabelOpts, t.inputLabelLen, t.inputBorderShape, true)
case reqRedrawHeaderLabel:
t.printLabel(t.headerBorder, t.headerLabel, t.headerLabelOpts, t.headerLabelLen, t.headerBorderShape, true)
if t.wborder != nil && t.inlineHeaderSepRow >= 0 {
t.wborder.DrawHSeparator(t.inlineHeaderSepRow, tui.WindowHeader)
t.printInlineLabel(t.wborder, t.inlineHeaderSepRow, t.headerLabel, t.headerLabelOpts, t.headerLabelLen)
}
case reqRedrawFooterLabel:
t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, true)
if t.wborder != nil && t.inlineFooterSepRow >= 0 {
t.wborder.DrawHSeparator(t.inlineFooterSepRow, tui.WindowFooter)
t.printInlineLabel(t.wborder, t.inlineFooterSepRow, t.footerLabel, t.footerLabelOpts, t.footerLabelLen)
}
case reqRedrawListLabel:
t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, true)
case reqRedrawBorderLabel:
+43
View File
@@ -1122,6 +1122,49 @@ func (w *LightWindow) DrawHBorder() {
w.drawBorder(true)
}
func (w *LightWindow) DrawHSeparator(row int, windowType WindowType) {
if w.height == 0 {
return
}
if w.border.shape == BorderNone {
return
}
colorFor := func(wt WindowType) ColorPair {
switch wt {
case WindowList:
return ColListBorder
case WindowInput:
return ColInputBorder
case WindowHeader:
return ColHeaderBorder
case WindowFooter:
return ColFooterBorder
case WindowPreview:
return ColPreviewBorder
}
return ColBorder
}
// Section color for the horizontal; list-border color (w.windowType) for the T-junctions.
lineColor := colorFor(windowType)
junctionColor := colorFor(w.windowType)
hw := runeWidth(w.border.top)
w.Move(row, 0)
if !w.border.shape.HasLeft() && !w.border.shape.HasRight() {
// No verticals to join, so draw a continuous horizontal across the full width.
full := max(0, w.width/hw)
rem := w.width - full*hw
w.CPrint(lineColor, repeat(w.border.top, full)+repeat(' ', rem))
return
}
lw := runeWidth(w.border.leftMid)
rw := runeWidth(w.border.rightMid)
inner := max(0, (w.width-lw-rw)/hw)
rem := (w.width - lw - rw) - inner*hw
w.CPrint(junctionColor, string(w.border.leftMid))
w.CPrint(lineColor, repeat(w.border.top, inner)+repeat(' ', rem))
w.CPrint(junctionColor, string(w.border.rightMid))
}
func (w *LightWindow) drawBorder(onlyHorizontal bool) {
if w.height == 0 {
return
+54
View File
@@ -1017,6 +1017,60 @@ func (w *TcellWindow) DrawHBorder() {
w.drawBorder(true)
}
func (w *TcellWindow) DrawHSeparator(row int, windowType WindowType) {
if w.height == 0 {
return
}
shape := w.borderStyle.shape
if shape == BorderNone {
return
}
styleFor := func(wt WindowType) tcell.Style {
if !w.color {
return w.normal.style()
}
switch wt {
case WindowBase:
return ColBorder.style()
case WindowList:
return ColListBorder.style()
case WindowHeader:
return ColHeaderBorder.style()
case WindowFooter:
return ColFooterBorder.style()
case WindowInput:
return ColInputBorder.style()
case WindowPreview:
return ColPreviewBorder.style()
}
return w.normal.style()
}
// Section color for the horizontal; list-border color (w.windowType) for the T-junctions
// so the outer frame's verticals stay visually continuous.
lineStyle := styleFor(windowType)
junctionStyle := styleFor(w.windowType)
y := w.top + row
left := w.left
right := left + w.width
hw := runeWidth(w.borderStyle.top)
hasVert := shape.HasLeft() || shape.HasRight()
if !hasVert {
// No verticals to join, so draw a continuous horizontal across the full width.
for x := left; x <= right-hw; x += hw {
_screen.SetContent(x, y, w.borderStyle.top, nil, lineStyle)
}
return
}
leftMidW := runeWidth(w.borderStyle.leftMid)
rightMidW := runeWidth(w.borderStyle.rightMid)
max := right - leftMidW - rightMidW
for x := left + leftMidW; x <= max; x += hw {
_screen.SetContent(x, y, w.borderStyle.top, nil, lineStyle)
}
_screen.SetContent(left, y, w.borderStyle.leftMid, nil, junctionStyle)
_screen.SetContent(right-rightMidW, y, w.borderStyle.rightMid, nil, junctionStyle)
}
func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
if w.height == 0 {
return
+25 -5
View File
@@ -595,11 +595,12 @@ const (
BorderBottom
BorderLeft
BorderRight
BorderInline
)
func (s BorderShape) HasLeft() bool {
switch s {
case BorderNone, BorderPhantom, BorderLine, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
case BorderNone, BorderPhantom, BorderLine, BorderInline, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
return false
}
return true
@@ -607,7 +608,7 @@ func (s BorderShape) HasLeft() bool {
func (s BorderShape) HasRight() bool {
switch s {
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
case BorderNone, BorderPhantom, BorderLine, BorderInline, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
return false
}
return true
@@ -615,7 +616,7 @@ func (s BorderShape) HasRight() bool {
func (s BorderShape) HasTop() bool {
switch s {
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
case BorderNone, BorderPhantom, BorderLine, BorderInline, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
return false
}
return true
@@ -623,7 +624,7 @@ func (s BorderShape) HasTop() bool {
func (s BorderShape) HasBottom() bool {
switch s {
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom
case BorderNone, BorderPhantom, BorderLine, BorderInline, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom
return false
}
return true
@@ -643,6 +644,8 @@ type BorderStyle struct {
topRight rune
bottomLeft rune
bottomRight rune
leftMid rune
rightMid rune
}
type BorderCharacter int
@@ -658,7 +661,9 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topLeft: ' ',
topRight: ' ',
bottomLeft: ' ',
bottomRight: ' '}
bottomRight: ' ',
leftMid: ' ',
rightMid: ' '}
}
if !unicode {
return BorderStyle{
@@ -671,6 +676,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '+',
bottomLeft: '+',
bottomRight: '+',
leftMid: '+',
rightMid: '+',
}
}
switch shape {
@@ -685,6 +692,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '┐',
bottomLeft: '└',
bottomRight: '┘',
leftMid: '├',
rightMid: '┤',
}
case BorderBold:
return BorderStyle{
@@ -697,6 +706,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '┓',
bottomLeft: '┗',
bottomRight: '┛',
leftMid: '┣',
rightMid: '┫',
}
case BorderBlock:
// ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜
@@ -712,6 +723,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '▜',
bottomLeft: '▙',
bottomRight: '▟',
leftMid: '▌',
rightMid: '▐',
}
case BorderThinBlock:
@@ -728,6 +741,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '🭾',
bottomLeft: '🭼',
bottomRight: '🭿',
leftMid: '▏',
rightMid: '▕',
}
case BorderDouble:
@@ -741,6 +756,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '╗',
bottomLeft: '╚',
bottomRight: '╝',
leftMid: '╠',
rightMid: '╣',
}
}
return BorderStyle{
@@ -753,6 +770,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '╮',
bottomLeft: '╰',
bottomRight: '╯',
leftMid: '├',
rightMid: '┤',
}
}
@@ -811,6 +830,7 @@ type Window interface {
DrawBorder()
DrawHBorder()
DrawHSeparator(row int, windowType WindowType)
Refresh()
FinishFill()
+32
View File
@@ -1392,5 +1392,37 @@ class TestLayout < TestInteractive
input: "(printf 'Xaa\\nYbb\\nZcc\\n'; seq 5)",
clicks: clicks)
end
# Inline header inside a rounded list border.
define_method(:"test_click_header_border_inline_#{slug}") do
opts = %(--layout=#{layout} --style full --header $'Aaa\\nBbb\\nCcc' )
verify_clicks(kind: :header, opts: opts, input: 'seq 5', clicks: HEADER_CLICKS)
end
# Inline header inside a horizontal list border (top+bottom only, no T-junctions).
define_method(:"test_click_header_border_inline_horizontal_list_#{slug}") do
opts = %(--layout=#{layout} --style full --header $'Aaa\\nBbb\\nCcc' )
verify_clicks(kind: :header, opts: opts, input: 'seq 5', clicks: HEADER_CLICKS)
end
# Inline header-lines inside a rounded list border.
define_method(:"test_click_header_lines_border_inline_#{slug}") do
clicks_hl = if layout == 'default'
[%w[Xaa 3], %w[Ybb 2], %w[Zcc 1]]
else
[%w[Xaa 1], %w[Ybb 2], %w[Zcc 3]]
end
opts = %(--layout=#{layout} --style full --header-lines 3 )
verify_clicks(kind: :header, opts: opts,
input: "(printf 'Xaa\\nYbb\\nZcc\\n'; seq 5)",
clicks: clicks_hl)
end
# Inline footer inside a rounded list border.
define_method(:"test_click_footer_border_inline_#{slug}") do
opts = %(--layout=#{layout} --style full --footer $'Foo\\nBar\\nBaz' )
verify_clicks(kind: :footer, opts: opts, input: 'seq 5',
clicks: [%w[Foo 1], %w[Bar 2], %w[Baz 3]])
end
end
end