Detail panes: path in the head, text flows around the poster
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+83
-44
@@ -2,55 +2,56 @@ package ui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
const (
|
||||
minCoverRows = 6 // a smaller poster is not worth showing
|
||||
minCoverCols = 8 //
|
||||
minTextRows = 10 // the poster never takes these rows from the text
|
||||
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: the head (title and facts) with
|
||||
// the poster to its right on top, and the plot below both. On panes too small
|
||||
// for the poster everything is one 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 // column: top row, plot
|
||||
*tview.Flex // holds the text; the poster is drawn on top of its corner
|
||||
app *App
|
||||
top *tview.Flex // row: head, cover
|
||||
head *tview.TextView
|
||||
text *tview.TextView
|
||||
cover *coverView
|
||||
plot *tview.TextView
|
||||
|
||||
text detailText
|
||||
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
|
||||
combined bool // head and plot are shown together in the plot view
|
||||
|
||||
// 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().SetDirection(tview.FlexRow),
|
||||
Flex: tview.NewFlex(),
|
||||
app: app,
|
||||
top: tview.NewFlex(),
|
||||
head: tview.NewTextView().SetDynamicColors(true).SetWordWrap(true),
|
||||
text: tview.NewTextView().SetDynamicColors(true).SetWrap(false),
|
||||
cover: newCoverView(app.coverStyle),
|
||||
plot: tview.NewTextView().SetDynamicColors(true).SetWordWrap(true),
|
||||
}
|
||||
d.cover.SetBorderPadding(0, 0, 1, 0)
|
||||
d.top.AddItem(d.head, 0, 1, false).AddItem(d.cover, 0, 0, false)
|
||||
d.AddItem(d.top, 0, 0, false).AddItem(d.plot, 0, 1, false)
|
||||
d.AddItem(d.text, 0, 1, false)
|
||||
d.SetBorder(true).SetTitle(" Details ")
|
||||
d.combined = true
|
||||
return d
|
||||
}
|
||||
|
||||
// Show displays the text and, if there is one, the poster.
|
||||
func (d *detailPane) Show(text detailText, ref coverRef) {
|
||||
d.text = text
|
||||
d.applyText()
|
||||
d.detail = text
|
||||
d.laidOut.valid = false
|
||||
if !d.app.covers || ref.fetch == nil {
|
||||
d.want, d.hasCover = "", false
|
||||
d.cover.setImage(nil)
|
||||
@@ -75,33 +76,71 @@ func (d *detailPane) Show(text detailText, ref coverRef) {
|
||||
// Clear empties the pane.
|
||||
func (d *detailPane) Clear() { d.Show(detailText{}, coverRef{}) }
|
||||
|
||||
// applyText puts the texts into their views for the current layout.
|
||||
func (d *detailPane) applyText() {
|
||||
if d.combined {
|
||||
d.head.SetText("")
|
||||
d.plot.SetText(d.text.String()).ScrollToBeginning()
|
||||
return
|
||||
}
|
||||
d.head.SetText(d.text.head).ScrollToBeginning()
|
||||
d.plot.SetText(d.text.plot).ScrollToBeginning()
|
||||
}
|
||||
|
||||
func (d *detailPane) Draw(screen tcell.Screen) {
|
||||
_, _, w, h := d.GetInnerRect()
|
||||
coverW, coverH := 0, 0
|
||||
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 plot keeps its rows.
|
||||
coverW, coverH = coverCells(aw, ah, w*2/5, h-minTextRows)
|
||||
if coverH < minCoverRows || coverW < minCoverCols {
|
||||
coverW, coverH = 0, 0
|
||||
// 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)
|
||||
if d.coverH < minCoverRows || d.coverW < minCoverCols {
|
||||
d.coverW, d.coverH = 0, 0
|
||||
}
|
||||
}
|
||||
if combined := coverW == 0; combined != d.combined {
|
||||
d.combined = combined
|
||||
d.applyText()
|
||||
|
||||
narrow, rows := w, 0
|
||||
if d.coverW > 0 {
|
||||
narrow, rows = w-d.coverW-1, d.coverH // one column of space left of the poster
|
||||
}
|
||||
d.ResizeItem(d.top, coverH, 0)
|
||||
d.top.ResizeItem(d.cover, coverW+1, 0) // +1: the padding left of the poster
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user