mirror of
https://github.com/junegunn/fzf.git
synced 2026-04-26 17:30:32 +08:00
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:
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user