Real-image posters: scale and encode in the background
Done in Draw it took about 200 ms per new poster (BiLinear scale to the box plus PNG encoding), so each step to another title stalled the UI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+70
-10
@@ -49,8 +49,27 @@ type kittyGraphics struct {
|
||||
// an image that is smaller than its placement (Ghostty shows it at its own
|
||||
// size, so a small poster came out smaller than the others).
|
||||
cellPixels func() (w, h int)
|
||||
|
||||
// background, if set, runs prepare off the UI goroutine and then ready on
|
||||
// it. Scaling and encoding a poster takes about 200 ms; done in Draw, every
|
||||
// step to a title with a poster not uploaded yet waited that long. Without
|
||||
// it (tests) posters are prepared at once.
|
||||
background func(prepare func() prepared, ready func(prepared))
|
||||
preparing map[footprint]bool
|
||||
done map[footprint]prepared // prepared, not uploaded yet
|
||||
}
|
||||
|
||||
// prepared is a poster encoded for upload.
|
||||
type prepared struct {
|
||||
encoded string // base64 PNG
|
||||
w, h int // its size in pixels
|
||||
err error
|
||||
}
|
||||
|
||||
// maxPrepared bounds the prepared posters that wait for a draw (the
|
||||
// selection moved on before they were ready).
|
||||
const maxPrepared = 8
|
||||
|
||||
// settleDelay is how long the window size must stay the same before real
|
||||
// images are shown again. While a window is dragged to another size, tens of
|
||||
// sizes go by; uploading and placing images for each of them races with the
|
||||
@@ -73,7 +92,8 @@ type footprint struct {
|
||||
}
|
||||
|
||||
func newKittyGraphics(write func([]byte) error) *kittyGraphics {
|
||||
return &kittyGraphics{write: write, nextID: 1, ids: map[footprint]uint32{}, latest: map[*image.RGBA]footprint{}}
|
||||
return &kittyGraphics{write: write, nextID: 1, ids: map[footprint]uint32{}, latest: map[*image.RGBA]footprint{},
|
||||
preparing: map[footprint]bool{}, done: map[footprint]prepared{}}
|
||||
}
|
||||
|
||||
// kittyAPC builds one graphics command: ESC _ G <keys> ; <payload> ESC \.
|
||||
@@ -90,26 +110,65 @@ func kittyAPC(keys string, payload []byte) []byte {
|
||||
}
|
||||
|
||||
// show makes sure the part rect of the image is uploaded and placed over
|
||||
// cols×rows cells, and returns the id the placeholder cells must carry. The same
|
||||
// footprint is reused; a different one for the same poster replaces the old
|
||||
// upload (which is removed from the terminal).
|
||||
// cols×rows cells, and returns the id the placeholder cells must carry, or 0
|
||||
// while the poster is still being prepared in the background (show is called
|
||||
// again on the redraw when it is ready). The same footprint is reused; a
|
||||
// different one for the same poster replaces the old upload (which is removed
|
||||
// from the terminal).
|
||||
func (k *kittyGraphics) show(img *image.RGBA, rect image.Rectangle, cols, rows int) (uint32, error) {
|
||||
key := footprint{img, rect, cols, rows}
|
||||
if id, ok := k.ids[key]; ok {
|
||||
return id, nil
|
||||
}
|
||||
var src image.Image = img.SubImage(rect)
|
||||
cw, ch := 0, 0
|
||||
if k.cellPixels != nil {
|
||||
if cw, ch := k.cellPixels(); cw > 0 && ch > 0 {
|
||||
src = scaleImage(src, cols*cw, rows*ch)
|
||||
cw, ch = k.cellPixels()
|
||||
}
|
||||
prepare := func() prepared { return encodePoster(img, rect, cols*cw, rows*ch) }
|
||||
p, ok := k.done[key]
|
||||
switch {
|
||||
case ok:
|
||||
delete(k.done, key)
|
||||
case k.background == nil:
|
||||
p = prepare()
|
||||
default:
|
||||
if !k.preparing[key] {
|
||||
k.preparing[key] = true
|
||||
k.background(prepare, func(p prepared) {
|
||||
delete(k.preparing, key)
|
||||
if len(k.done) >= maxPrepared {
|
||||
clear(k.done)
|
||||
}
|
||||
k.done[key] = p
|
||||
})
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
if p.err != nil {
|
||||
return 0, p.err
|
||||
}
|
||||
return k.upload(key, p)
|
||||
}
|
||||
|
||||
// encodePoster scales the part rect of img to w×h pixels (if both are known)
|
||||
// and encodes it. Safe for use from any goroutine.
|
||||
func encodePoster(img *image.RGBA, rect image.Rectangle, w, h int) prepared {
|
||||
var src image.Image = img.SubImage(rect)
|
||||
if w > 0 && h > 0 {
|
||||
src = scaleImage(src, w, h)
|
||||
}
|
||||
var data bytes.Buffer
|
||||
enc := png.Encoder{CompressionLevel: png.BestSpeed}
|
||||
if err := enc.Encode(&data, src); err != nil {
|
||||
return 0, err
|
||||
return prepared{err: err}
|
||||
}
|
||||
encoded := base64.StdEncoding.EncodeToString(data.Bytes())
|
||||
return prepared{encoded: base64.StdEncoding.EncodeToString(data.Bytes()), w: src.Bounds().Dx(), h: src.Bounds().Dy()}
|
||||
}
|
||||
|
||||
// upload sends a prepared poster and places it.
|
||||
func (k *kittyGraphics) upload(key footprint, p prepared) (uint32, error) {
|
||||
img, cols, rows := key.img, key.cols, key.rows
|
||||
encoded := p.encoded
|
||||
id := k.nextID
|
||||
for first, rest := true, encoded; len(rest) > 0; first = false {
|
||||
n := min(len(rest), kittyChunk)
|
||||
@@ -143,7 +202,7 @@ func (k *kittyGraphics) show(img *image.RGBA, rect image.Rectangle, cols, rows i
|
||||
k.latest[img] = key
|
||||
if k.debug != nil {
|
||||
k.debug(fmt.Sprintf("graphics: id %d, poster %dx%d px cropped to %v, sent as %dx%d px, placed over %dx%d cells",
|
||||
id, img.Bounds().Dx(), img.Bounds().Dy(), rect, src.Bounds().Dx(), src.Bounds().Dy(), cols, rows))
|
||||
id, img.Bounds().Dx(), img.Bounds().Dy(), key.rect, p.w, p.h, cols, rows))
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
@@ -156,6 +215,7 @@ func (k *kittyGraphics) deleteAll() {
|
||||
}
|
||||
k.ids = map[footprint]uint32{}
|
||||
k.latest = map[*image.RGBA]footprint{}
|
||||
clear(k.done) // posters in preparation still arrive, but are not used without a draw asking for them
|
||||
}
|
||||
|
||||
// placeholderCell returns what to draw in the cell at (row, col) of the image
|
||||
|
||||
Reference in New Issue
Block a user