Posters: always a 2:3 box

The poster box followed the image's shape, so images Radarr/Sonarr hand
out in another shape came out smaller than the others in a list. The box
is now always 2:3: an image within 12% of that fills it, trimmed at the
long sides (block characters too, which were stretched before); one
further off is shown whole, centred in the box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 11:15:26 +02:00
co-authored by Claude Opus 5.5
parent 1056846a48
commit 786e783951
4 changed files with 84 additions and 12 deletions
+34 -11
View File
@@ -311,13 +311,26 @@ func (c *coverView) aspectOfCells() float64 {
func (c *coverView) setImage(img *image.RGBA) { c.img, c.note, c.cells = img, "", nil }
func (c *coverView) setNote(note string) { c.img, c.note, c.cells = nil, note, nil }
// aspect returns the width and height of the poster (2:3 until one is loaded).
func (c *coverView) aspect() (w, h int) {
if c.img == nil {
return 2, 3
// Every poster gets a box of the usual poster shape, 2:3, so all posters of a
// list have the same size whatever image Radarr/Sonarr hand out.
const (
posterW, posterH = 2, 3
// posterShapeTolerance is how far an image's shape may differ from 2:3 and
// still fill the box (trimmed at the long sides); one further off is shown
// whole inside it.
posterShapeTolerance = 0.12
)
// aspect returns the shape of the box the poster is shown in.
func (c *coverView) aspect() (w, h int) { return posterW, posterH }
// nearPosterShape reports whether an image is close enough to 2:3 to fill the box.
func nearPosterShape(b image.Rectangle) bool {
if b.Dx() <= 0 || b.Dy() <= 0 {
return false
}
b := c.img.Bounds()
return b.Dx(), b.Dy()
d := float64(b.Dx())*posterH/(float64(b.Dy())*posterW) - 1
return d >= -posterShapeTolerance && d <= posterShapeTolerance
}
// coverFitTolerance is how far the shape of the cell box may differ from the
@@ -376,19 +389,26 @@ func (c *coverView) Draw(screen tcell.Screen) {
return
}
// The 2:3 box, right-aligned in the area. An image close to that shape
// fills it (trimmed); one far from it (square, landscape) is shown whole,
// centred in the box.
b := c.img.Bounds()
cols, rows := coverCells(b.Dx(), b.Dy(), w, h, c.aspectOfCells())
boxW, boxH := coverCells(posterW, posterH, w, h, c.aspectOfCells())
cols, rows := boxW, boxH
if !nearPosterShape(b) {
cols, rows = coverCells(b.Dx(), b.Dy(), boxW, boxH, c.aspectOfCells())
}
if cols < 1 || rows < 1 {
return
}
if c.drawGraphics(screen, x+w-cols, y, cols, rows) {
x += w - boxW + (boxW-cols)/2
y += (boxH - rows) / 2
if c.drawGraphics(screen, x, y, cols, rows) {
return
}
if c.cells == nil || c.cols != cols || c.rows != rows {
c.cols, c.rows, c.cells = cols, rows, c.build(cols, rows)
}
// Right-align the poster inside its area, so a narrower poster sits against the edge.
x += w - cols
for row := 0; row < rows; row++ {
for col := 0; col < cols; col++ {
cell := c.cells[row*cols+col]
@@ -445,7 +465,10 @@ func (c *coverView) drawGraphics(screen tcell.Screen, x, y, cols, rows int) bool
// build renders the poster into cols×rows cells.
func (c *coverView) build(cols, rows int) []styledCell {
st := c.style
scaled := boxScale(c.img, cols*st.sw, rows*st.sh)
// Trim the image to the shape of the cells first, as for real images, so
// it is not stretched.
crop := cropToBox(c.img.Bounds(), cols, rows, c.aspectOfCells())
scaled := boxScale(c.img.SubImage(crop), cols*st.sw, rows*st.sh)
cells := make([]styledCell, 0, cols*rows)
px := make([]color.RGBA, st.sw*st.sh)
for row := 0; row < rows; row++ {