1
0
Fork 0
mirror of https://github.com/diamondburned/cchat-gtk.git synced 2025-01-11 04:56:47 +00:00
cchat-gtk/internal/gts/httputil/image.go
diamondburned (Forefront) 2fae6ffbb3 UI changes; typing state working
This commit refactors the input container's UI as well as fixing some
bugs related to asynchronous fetching of images.

It also adds complete typing indicator capabilities, all without using a
single mutex!
2020-07-08 02:17:54 -07:00

157 lines
3.5 KiB
Go

package httputil
import (
"context"
"io"
"strings"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
type ImageContainer interface {
SetFromPixbuf(*gdk.Pixbuf)
SetFromAnimation(*gdk.PixbufAnimation)
Connect(string, interface{}, ...interface{}) (glib.SignalHandle, error)
// for internal use
pbgetter
}
type ImageContainerSizer interface {
ImageContainer
SetSizeRequest(w, h int)
}
// AsyncImage loads an image. This method uses the cache.
func AsyncImage(img ImageContainer, url string, procs ...imgutil.Processor) {
if url == "" {
return
}
ctx, cancel := context.WithCancel(context.Background())
connectDestroyer(img, cancel)
gif := strings.Contains(url, ".gif")
l, err := gdk.PixbufLoaderNew()
if err != nil {
log.Error(errors.Wrap(err, "Failed to make pixbuf loader"))
return
}
l.Connect("area-prepared", areaPreparedFn(ctx, img, gif))
go syncImage(ctx, l, url, procs, gif)
}
// AsyncImageSized resizes using GdkPixbuf. This method uses the cache.
func AsyncImageSized(img ImageContainerSizer, url string, w, h int, procs ...imgutil.Processor) {
if url == "" {
return
}
// Add a processor to resize.
procs = append(procs, imgutil.Resize(w, h))
ctx, cancel := context.WithCancel(context.Background())
connectDestroyer(img, cancel)
gif := strings.Contains(url, ".gif")
l, err := gdk.PixbufLoaderNew()
if err != nil {
log.Error(errors.Wrap(err, "Failed to make pixbuf loader"))
return
}
l.Connect("size-prepared", func(l *gdk.PixbufLoader, imgW, imgH int) {
w, h = imgutil.MaxSize(imgW, imgH, w, h)
if w != imgW || h != imgH {
l.SetSize(w, h)
execIfCtx(ctx, func() { img.SetSizeRequest(w, h) })
}
})
l.Connect("area-prepared", areaPreparedFn(ctx, img, gif))
go syncImage(ctx, l, url, procs, gif)
}
type pbgetter interface {
GetPixbuf() *gdk.Pixbuf
GetAnimation() *gdk.PixbufAnimation
GetStorageType() gtk.ImageType
}
var _ pbgetter = (*gtk.Image)(nil)
func connectDestroyer(img ImageContainer, cancel func()) {
img.Connect("destroy", func(img ImageContainer) {
cancel()
img.SetFromPixbuf(nil)
})
}
func areaPreparedFn(ctx context.Context, img ImageContainer, gif bool) func(l *gdk.PixbufLoader) {
return func(l *gdk.PixbufLoader) {
if !gif {
p, err := l.GetPixbuf()
if err != nil {
log.Error(errors.Wrap(err, "Failed to get pixbuf"))
return
}
execIfCtx(ctx, func() { img.SetFromPixbuf(p) })
} else {
p, err := l.GetAnimation()
if err != nil {
log.Error(errors.Wrap(err, "Failed to get animation"))
return
}
execIfCtx(ctx, func() { img.SetFromAnimation(p) })
}
}
}
func execIfCtx(ctx context.Context, fn func()) {
gts.ExecAsync(func() {
if ctx.Err() == nil {
fn()
}
})
}
func syncImage(ctx context.Context, l io.WriteCloser, url string, p []imgutil.Processor, gif bool) {
// Close at the end when done.
defer l.Close()
r, err := get(ctx, url, true)
if err != nil {
log.Error(err)
return
}
defer r.Body.Close()
// If we have processors, then write directly in there.
if len(p) > 0 {
if !gif {
err = imgutil.ProcessStream(l, r.Body, p)
} else {
err = imgutil.ProcessAnimationStream(l, r.Body, p)
}
} else {
// Else, directly copy the body over.
_, err = io.Copy(l, r.Body)
}
if err != nil {
log.Error(errors.Wrap(err, "Error processing image"))
return
}
}