Files
arr-tui/internal/ui/detail.go
T

151 lines
4.3 KiB
Go

package ui
import (
"image"
"strings"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
const (
coverGapCols = 2 // empty columns between the poster and the text left of it
coverGapRows = 1 // empty rows between the poster and the text below it
minCoverRows = 6 // a smaller poster is not worth showing
minCoverCols = 8
minTextRows = 6 // the poster never takes these rows from the text
)
// detailPane shows the details of one title as one flowing text with the
// poster floating at the top right: the head (title and facts) and the plot
// run along the poster on the left and continue at full width below it. On
// panes too small for the poster the text simply uses the full width.
type detailPane struct {
*tview.Flex // holds the text; the poster is drawn on top of its corner
app *App
text *tview.TextView
cover *coverView
detail detailText
want string // key of the poster that should be shown
hasCover bool // a poster (or its loading note) is part of the content
// What the text was last wrapped for.
laidOut struct {
width, narrow, rows int
valid bool
}
coverW, coverH int // size of the poster on screen, 0 = hidden
}
func newDetailPane(app *App) *detailPane {
d := &detailPane{
Flex: tview.NewFlex(),
app: app,
text: tview.NewTextView().SetDynamicColors(true).SetWrap(false),
cover: newCoverView(app.coverStyle, app.gfx, func() float64 { return app.cellAspect }),
}
d.AddItem(d.text, 0, 1, false)
d.SetBorder(true).SetTitle(" Details ")
return d
}
// Show displays the text and, if there is one, the poster.
func (d *detailPane) Show(text detailText, ref coverRef) {
d.detail = text
d.laidOut.valid = false
if !d.app.covers || ref.fetch == nil {
d.want, d.hasCover = "", false
d.cover.setImage(nil)
return
}
d.want, d.hasCover = ref.key, true
d.cover.setNote("loading cover…")
want := ref.key
d.app.loadCover(ref, func(img *image.RGBA, err error) {
if d.want != want {
return // the selection moved on
}
if err != nil {
d.hasCover = false // nothing to show: give the room back to the text
d.cover.setImage(nil)
return
}
d.cover.setImage(img)
})
}
// Clear empties the pane.
func (d *detailPane) Clear() { d.Show(detailText{}, coverRef{}) }
func (d *detailPane) Draw(screen tcell.Screen) {
x, y, w, h := d.GetInnerRect()
d.coverW, d.coverH = 0, 0
if d.hasCover {
aw, ah := d.cover.aspect()
// At most 40% of the width, and the text keeps some rows below.
d.coverW, d.coverH = coverCells(aw, ah, w*2/5, h-minTextRows, d.cover.aspectOfCells())
if d.coverH < minCoverRows || d.coverW < minCoverCols {
d.coverW, d.coverH = 0, 0
}
}
narrow, rows := w, 0
if d.coverW > 0 {
// The text keeps clear of the poster: columns to its left, and one more
// line is wrapped narrow so nothing sits right under it.
narrow, rows = w-d.coverW-coverGapCols, d.coverH+coverGapRows
}
if !d.laidOut.valid || d.laidOut.width != w || d.laidOut.narrow != narrow || d.laidOut.rows != rows {
d.laidOut.width, d.laidOut.narrow, d.laidOut.rows, d.laidOut.valid = w, narrow, rows, true
d.text.SetText(strings.Join(flowWrap(d.detail.String(), narrow, w, rows), "\n"))
}
d.Flex.Draw(screen)
if d.coverW > 0 {
d.cover.SetRect(x+w-d.coverW, y, d.coverW, d.coverH)
d.cover.Draw(screen)
}
}
// flowWrap word-wraps text for a column that is narrow for its first
// narrowRows lines and wide afterwards, like text flowing around a picture
// floating at the top right. Newlines in the text are kept. Color tags have
// no width and carry over from line to line.
func flowWrap(text string, narrow, wide, narrowRows int) []string {
var lines []string
var cur strings.Builder
curW := 0
limit := func() int {
if len(lines) < narrowRows {
return max(narrow, 1)
}
return max(wide, 1)
}
flush := func() {
lines = append(lines, cur.String())
cur.Reset()
curW = 0
}
for _, para := range strings.Split(text, "\n") {
for _, word := range strings.Fields(para) {
ww := tview.TaggedStringWidth(word)
if curW > 0 && curW+1+ww > limit() {
flush()
}
if curW > 0 {
cur.WriteByte(' ')
curW++
}
cur.WriteString(word)
curW += ww
}
flush() // end of paragraph
}
for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" {
lines = lines[:len(lines)-1]
}
return lines
}