108 lines
2.7 KiB
Go
108 lines
2.7 KiB
Go
package ui
|
|
|
|
import (
|
|
"github.com/lixenwraith/terminal"
|
|
"github.com/lixenwraith/terminal/tui"
|
|
)
|
|
|
|
// Panel is a modal overlay with a scrollable body. It owns the frame, the
|
|
// scroll state and the scrollbar; the caller renders rows into the region
|
|
// Render returns, starting at Scroll.
|
|
type Panel struct {
|
|
Title string
|
|
Hint string
|
|
Status string // line inside the bottom of the frame, e.g. a full filename
|
|
W, H int // 0 = 80% of the screen
|
|
Cursor bool // track a selected row rather than scrolling freely
|
|
|
|
Sel int
|
|
Scroll int
|
|
Rows int // content rows, set by the caller before Render
|
|
View int // visible rows, set by Render
|
|
}
|
|
|
|
// Move advances the selection, or the scroll offset when Cursor is unset.
|
|
func (p *Panel) Move(d int) {
|
|
if p.Cursor {
|
|
p.Sel += d
|
|
} else {
|
|
p.Scroll += d
|
|
}
|
|
p.clamp()
|
|
}
|
|
|
|
// Page moves by n screens.
|
|
func (p *Panel) Page(n int) { p.Move(n * max(1, p.View)) }
|
|
|
|
// Half moves by n half screens.
|
|
func (p *Panel) Half(n int) { p.Move(n * max(1, p.View/2)) }
|
|
|
|
// First jumps to the top.
|
|
func (p *Panel) First() { p.Sel, p.Scroll = 0, 0 }
|
|
|
|
// Last jumps to the bottom.
|
|
func (p *Panel) Last() {
|
|
p.Sel, p.Scroll = p.Rows-1, p.Rows
|
|
p.clamp()
|
|
}
|
|
|
|
// Reset returns the panel to the top, for new content.
|
|
func (p *Panel) Reset() { p.Sel, p.Scroll = 0, 0 }
|
|
|
|
func (p *Panel) clamp() {
|
|
if p.Rows <= 0 {
|
|
p.Sel, p.Scroll = 0, 0
|
|
return
|
|
}
|
|
p.Sel = tui.ClampCursor(p.Sel, p.Rows)
|
|
if p.Cursor {
|
|
p.Scroll = tui.AdjustScroll(p.Sel, p.Scroll, p.View, p.Rows)
|
|
}
|
|
p.Scroll = tui.ClampScroll(p.Scroll, p.View, p.Rows)
|
|
}
|
|
|
|
// Render draws the frame and returns the body region, scrollbar excluded.
|
|
// A zero-width result means the screen is too small to show the panel.
|
|
func (p *Panel) Render(root tui.Region, th *Theme) tui.Region {
|
|
w, h := p.W, p.H
|
|
if w <= 0 {
|
|
w = root.W * 80 / 100
|
|
}
|
|
if h <= 0 {
|
|
h = root.H * 80 / 100
|
|
}
|
|
w, h = min(w, root.W-4), min(h, root.H-2)
|
|
if w < 24 || h < 6 {
|
|
return tui.Region{}
|
|
}
|
|
|
|
t, hint := p.Title, p.Hint
|
|
if t != "" {
|
|
t = " " + t + " "
|
|
}
|
|
if hint != "" {
|
|
hint = " " + hint + " "
|
|
}
|
|
|
|
content := tui.Center(root, w, h).Modal(tui.ModalOpts{
|
|
Title: t, Hint: hint, Border: tui.LineDouble,
|
|
BorderFg: th.Accent, TitleFg: th.HeaderFg, HintFg: th.HintFg, Bg: th.FocusBg,
|
|
})
|
|
|
|
body := content
|
|
if p.Status != "" && content.H > 3 {
|
|
body = content.Sub(0, 0, content.W, content.H-2)
|
|
content.HLine(content.H-2, tui.LineSingle, th.Border)
|
|
content.Text(0, content.H-1, tui.Truncate(p.Status, content.W),
|
|
th.HintFg, th.FocusBg, terminal.AttrDim)
|
|
}
|
|
|
|
p.View = body.H
|
|
p.clamp()
|
|
if p.Rows > body.H && body.W > 2 {
|
|
body.Sub(body.W-1, 0, 1, body.H).ScrollBar(0, p.Scroll, body.H, p.Rows, th.Border)
|
|
body = body.Sub(0, 0, body.W-1, body.H)
|
|
}
|
|
return body
|
|
}
|